← Home

Migrating to async/await (Node.js / AWS Lambda / Serverless Framework)

As I mentioned (//2017/07/27/serverless-web-apps-without-client-side-javascript) I’m using the Serverless Framework (serverless.com) to build a new product using Node.js.

I’m fairly new to node, but there was a feature I was really missing, and that’s async/await which I think makes Node.js much less cluttered with boilerplate syntax than using callbacks or promises.

When I first tried to use it I got a syntax error. Of course! I should have noticed that the version of Node.js that AWS Lambda currently supports (6.10.3) doesn’t include async/await.

Surely there’s an easy way to get async/await to work with Serverless?

What are callbacks, promises and async/await anyway?

Callbacks came first, then Promises (in their various guises) then finally async/await. It looks like quite a bit of a mess to anyone who’s coming fresh to Node.js, but on the other hand, I’ve been using a similar feature in C# since 2012, and it’s not really any different to the situation there - delegates / event handlers for handling events in the early days (callbacks), then later lambda expressions and Task were added (promises), then the language got async / await on top to make the code clearer.

Since there’s 3 ways of achieving the same objective, Node.js libraries use all of the different mechanisms depending on how old they are.

I must have missed the official promises developments entirely, since there were a number of various ways of doing it floating about. Fortunately there’s a really good interactive guide at [0] which explains how the different systems work and why they’re important. This was really helpful for me.

This blog was also handy to guide me in how I could convert code I’d written which used callbacks to using promises. [1]

Once I had my code using Promises, I wanted to step up to async/await to simplify more. Since I can’t update Node.js on AWS, I’d need to update my code somehow.

Babel steps up

From doing a bit of React.js, I knew that modern javascript code needs to be “transpiled” (i.e. modified / converted) to run on anything but the very latest Web browsers and that Babel [2] is often used to do this.

So, I knew I needed to get Babel to convert my modern javascript to javascript suitable to run on an older Node.js runtime.

I didn’t find Babel’s documentation particularly helpful, and a lot of the suggestions on Stackoverflow etc. are out-of-date with current best practice, but thanks to a few issues and questions on Stackoverflow, I worked out what I needed to do:

  • Install Babel into my project (npm install --save-dev babel-cli and npm install --save-dev babel-preset-env)

  • Add a .babelrc file to tell Babel what rules to apply (in my case, compile to the version of node that the current environment is using - which I’ve also set to be the same version that AWS Lambda uses)

    • You’ve got to be careful here. If the Node.js version on the machine that’s transpiling the code is newer than the target version, then Babel won’t do the correct operations.
  • Update the packages.json to add scripts in to:

    • Run the Babel executable against my Node.js code

      • Ignore the node_modules folder so that it doesn’t attempt to transpile everything.
      • Ignore the output folder.
    • Run tests against the newly transpiled output.

  • Update the Serverless.yml to use the transpiled output (under the /lib directory) which is compatible with the AWS Node.js version.

Migrating the code to use async/await

Once I had support for it, I needed to refactor my code. Here’s an example of the process I used to migrate some code I wrote to read from Google’s Geolocation API and parse the output into a Location class.

Callbacks

module.exports.postcodes = (apiKey, postCode, callback) => {
  const url = `https://maps.googleapis.com/maps/api/geocode/json?address=${escape(stripWhitespace(postCode))}&key=${escape(apiKey)}`;

  console.log(`postcode: ${url}`);

  fetch(url)
    .then(response => response.json())
    .then((s) => {
      console.log(`postcode: retrieved data: ${JSON.stringify(s)}`);
      const rv = new Location(s.results[0].geometry.location.lat,
        s.results[0].geometry.location.lng);
      callback(null, rv);
    })
    .catch((error) => {
      console.log(`postcode: error: ${error}`);
      callback(error, null);
    });
};

Promises

module.exports.postcodes = (apiKey, postCode) => new Promise((resolve, reject) => {
  const url = `https://maps.googleapis.com/maps/api/geocode/json?address=${escape(stripWhitespace(postCode))}&key=${escape(apiKey)}`;

  console.log(`postcode: ${url}`);

  fetch(url)
    .then(response => response.json())
    .then((s) => {
      console.log(`postcode: retrieved data: ${JSON.stringify(s)}`);
      resolve(createLocationFromGoogleResponse(s));
    })
    .catch((error) => {
      console.log(`postcode: error: ${error}`);
      reject(error);
    });
});

async/await

module.exports.postcodes = async (apiKey, postCode) => {
  const url = `https://maps.googleapis.com/maps/api/geocode/json?address=${escape(stripWhitespace(postCode))}&key=${escape(apiKey)}`;
  console.log(`postcode: ${url}`);
  const response = await fetch(url);
  const locationData = await response.json();
  console.log(`postcode: retrieved data: ${JSON.stringify(locationData)}`);
  return createLocationFromGoogleResponse(locationData);
};

Usage

function exampleCallback(a, b, callback) {
	callback(null, a+b);
}

function examplePromise(a, b) {
	return new Promise((resolve, reject) => resolve(a + b));
}

exampleCallback(1, 2, function(err, result) {
	console.log(result);
});

// Doesn't work at all. Callbacks and promises are not backwards compatible.
examplePromise(3, 4, function(err, result) {
        console.log(result);
});

// Promises are handled with then and catch.
examplePromise(5, 6)
	.then(results => console.log(results))
	.catch(err => console.log(err));

// But it's possible to await the promise instead inside an async function.
// console.log(await examplePrommise(7, 8));

The usage example shows that it’s important to note that the API surface changes when migrating from callbacks to Promises, so calling code will be needed for that, just not from when migrating to async / await.

Conclusion

I think it’s worth the effort of back-porting async/await to AWS Lambda to make code much easier to read, despite the added complication of a build step.

Sources