Developers & Practitioners
Avoiding GCF anti-patterns part 4: How to handle Promises correctly in your Node.js Cloud Function
Editor's note: Over the next several weeks, you'll see a series of blog posts focusing on best practices for writing Google Cloud Functions based on common questions or misconceptions as seen by the Support team. We refer to these as "anti-patterns" and offer you ways to avoid them. This article is the fourth post in the series.
Scenario
You notice that the data your Function saves to a database is either "undefined" or it is saving a cached value. For example, you have a Cloud Task that every hour invokes a Cloud Function that retrieves data from one database, transforms that data, and then saves the modified data to another database. Yet, you notice that your data is either undefined or a cached value.
Most common root issue
An unhandled promise.
One common anti-pattern we notice when using the then-able approach in a Cloud Function is that it is easy to overlook an unhandled Promise. For example, can you spot the issue in the following example?
exports.callToSlowRespondingAPI = (req, res) => {
getTheDataFromSomewhereThatTakesAwhile()
.then((response) => {
try {
let dataNowTransformed = transformData(response.data);
saveDataToDatabase(dataNowTransformed);
} catch (error) {
console.error("error getting data from somewhere that takes awhile:", error);
}
})
.then(() => {
res.status(200).send("save completed successfully");
});
}
You will not know how long the call transformData(response.data)
will take. And more likely, this call is probably an async method. The result is that the saveDataToDatabase
method is executed before transformData
()
has completed. Thus, the variable dataNowTransformed
is undefined upon saving to the database.
How to investigate
- Are you using
await
? If not, we recommend using async and await keywords to help improve the readability of your code. - If you cannot convert to await at this time, you'll need to add a logging layer (see example below) to determine if you have an unhandled promise.
How to add a logging layer using then-able functions
There are two ways to log:
- sync logging by modifying your callbacks
- async logging to avoid modifying your callbacks (i.e. adding a logging layer using then-able functions)
Suppose you are okay with modifying your callbacks. You can add a synchronous logging layer just by using console.log()
. We recommend creating a synchronous method called logData
()
function to keep your code clean and avoid numerous console.log
()
statements throughout your code.
getTheDataFromSomewhereThatTakesAwhile()
.then((response) => {
// synchronous logging layer here shows data before transforming
logData(response.data);
// now transform the data
let dataNowTransformed = transformData(response.data);
// synchronous logging layer here shows how data is transformed before sending on
logData(dataNowTransformed);
// send data to the next then() statement
return dataNowTransformed;
})
.then((dataNowTransformed) => {
// and then do something with your transformed data...
where logData()
looks like:
function logData(data) {
console.log("logging data: ", data);
}
Now suppose you do not want to modify the code within your callbacks. We recommend adding an async logging method as follows:
1. Create an async logDataAsync()
method in your Function.
// the async keyword here creates a promise which allows you to call .then()
async function logDataAsync(data) {
console.log(data);
return data; // this return is needed to pass data to the next .then()
}
2. Call the logDataAsync
method using the then-able() approach
getTheDataFromSomewhereThatTakesAwhile()
.then((response) => {
let dataNowTransformed = transformData(response.data);
return dataNowTransformed;
})
.then((dataNowTransformed) => {
return logDataAsync(dataNowTransformed);
})
.then((dataNowTransformed) => {
// and then do something with your transformed data...
});
Please see the helpful tips section for a more compact way to apply the async logging approach.
How to handle the Promise using the then-able approach
We recommend that you perform one task at a time within a .then()
callback. Going back to our original anti-pattern example, let's update it to use the then-able approach. Here's an end-to-end working example:
var axios = require("axios");
exports.callToSlowRespondingAPI = (req, res) => {
getTheDataFromSomewhereThatTakesAwhile()
.then((dataToBeTransformed) => {
return logDataAsync(dataToBeTransformed);
})
.then((dataToBeTransformed) => {
// one common pattern is to
// 1) add an async logging layer to log original data
// 2) return a promise
// 3) add an async logging layer to log transformed data
// 4) let the next then-able layer handle the Promise and
// pass the results along to the next method (saveDataToDatabase)
let dataNowTransformed = transformData(dataToBeTransformed);
return dataNowTransformed;
})
.then((dataNowTransformed) => {
return logDataAsync(dataNowTransformed);
})
.then((dataNowTransformed) => {
saveDataToDatabase(dataNowTransformed);
})
.then(() => {
res.status(200).send("Save completed successfully");
})
.catch((error) => {
console.error(error);
res.status(500).send("An unexpected error occurred");
});
};
async function getTheDataFromSomewhereThatTakesAwhile() {
// making an axios HTTPs call to a Cloud Function that takes 10 seconds to respond
// to simulate an outbound connection that takes a while
const response = await axios.get(
"https://us-west2-antipatterns-functions.cloudfunctions.net/respondAfter10Seconds"
);
// when using an async function, we can return the data directly
return response.data;
}
// the async keyword here creates a promise which allows you to call .then()
async function logDataAsync(data) {
console.log("logging data: ");
console.log(data);
return data; // this return is needed to pass data to the next .then()
}
// transform data
async function transformData(dataToBeTransformed) {
// uppercasing data to transform it
// this return is needed to pass data to the next .then()
return dataToBeTransformed.toUpperCase();
}
// pretending to save to a database
async function saveDataToDatabase(data) {
console.log("saving data to database:", data);
}
But all these back to back .then()
calls (called Promise chaining) make the code difficult to read and maintain. Please see the helpful tips section for a more compact way to write this code, in case you see it elsewhere.
If possible, we suggest that you use awaits. Notice how the code in the Function event handler callToSlowRespondingAPI
now becomes more succinct and readable. In addition, if anything goes wrong in these async method calls, an exception is thrown, in lieu of returning null or false in the return statement.
// note the async in the method signature
exports.callToSlowRespondingAPI = async (req, res) => {
try {
logData(response.data); //you could also use the async version here instead
let data = await getTheDataFromSomewhereThatTakesAwhile();
let transformedData = await transformData(data);
await saveDataToDatabase(transformData);
res.status(200).send("save completed successfully");
} catch (error) {
console.error(error);
res.status(500).send("something went wrong. please check logs");
}
};
async function getTheDataFromSomewhereThatTakesAwhile() {
// making an axios HTTPs call to a Cloud Function that takes 10 seconds to respond
// to simulate an outbound connection that takes a while
const response = await axios.get(
"https://us-west2-antipatterns-functions.cloudfunctions.net/respondAfter10Seconds"
);
// when using an async function, we can return the data directly
return response.data;
}
Other helpful tips
- Are you testing locally using the Functions Framework? You can follow this codelab to learn how to debug Node.js functions locally in Visual Studio Code.
- Whenever you are logging data, be aware of how much data you are logging (see log size limits) and whether your data has any sensitive information or personally identifiable information.
- Using the then-able() approach, you might often see code written as follows. This is functionally equivalent to the longer then-able() Promise-chaining version used above. However, we recommend using the async await approach for readability.
exports.callToSlowRespondingAPI = (req, res) => {
getTheDataFromSomewhereThatTakesAwhile()
.then(logDataAsync)
.then(transformData)
.then(logDataAsync)
.then(saveDataToDatabase)
.then(() => {
res.status(200).send("Save completed successfully");
})
.catch((error) => {
console.error(error);
res.status(500).send("An unexpected error occurred");
})
};
Recommended for you
Developers & Practitioners
Avoiding GCF anti-patterns part 1: How to write event-driven Cloud Functions properly by coding with idempotency in mind
First post in a series on how to avoid anti-patterns in Google Cloud Functions as seen by the Support team. This post explores what idempotent functions are and how this design pattern is important for background-triggered functions.
Developers & Practitioners
Avoiding GCF anti-patterns part 2: How to reuse Cloud Function instances for future invocations
This post explores what global scope is for a Cloud Function, when to use, and what issues to look out for when used incorrectly.
Developers & Practitioners
Avoiding GCF anti-patterns part 3: How to establish outbound connections correctly
Third post in a series on how to avoid anti-patterns in Google Cloud Functions as seen by the Support team. This post explores how to make outbound connections correctly by using explicit timeouts on your outbound calls.