JavaScript | NodeJS

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

  1. Are you using await? If not, we recommend using async and await keywords to help improve the readability of your code.
  2. 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: 

  1. sync logging by modifying your callbacks
  2. 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");
   })
};

By: Sara Ford (Cloud Developer Advocate) and Martin Skoviera (Technical Solutions Engineer)
Source: Google Cloud Blog

Total
0
Shares
Previous Article
Google Cloud | Training

A Learning Journey For Members Transitioning Out Of The Military

Next Article

Update On Google Cloud’s Work With The U.S. Government

Related Posts