Categories
Node.js

Are you using promises and async / await safely in Node.js?

5 min read

You’re building your Node.js server applications with a framework, and using promises and async / await. Mostly everything’s working well, but it can get confusing at times, especially when you need to handle errors. How do you know if you’re writing your async code correctly? And what happens if you’re not?!

Promises and async / await are a fundamental part of writing Node.js applications with modern JavaScript. If only you could be confident that you’re using them correctly in your applications. And wouldn’t it be great to know that your framework has got you covered when you mess up? (hey, we all do!)

If a framework has "native support" for promises and async / await then things are in good shape, but what does that actually mean? In this article I’ll bring you up to speed on what you need from your framework. Then we’ll run through which frameworks have native support, and look at how you can write async code safely in Node.js.

In this article I’m going to refer to promises and async / await collectively as "async code".

Support for async code in Node.js

Node.js has had support for promises since v4.0.0 (released September 2015) and async / await since v7.6.0 (released February 2017). These JavaScript language features are now widely used across the Node.js ecosystem.

What does framework “native support” for async code look like?

In this context, "native support" means that a framework supports a certain set of behaviours or features by default, without the need for you to add any extra code.

Here are the things which a Node.js framework should natively support for your async code to function safely and correctly:

  • Your route handler functions can use async / await – The framework will explicitly wait for the promise wrapping the route handler function to resolve or reject (all JavaScript functions using the async keyword are automatically wrapped in a promise).

  • You can throw errors from within a route handler function – The error will be caught and gracefully handled by the framework i.e. sending back an appropriate error response to the client and logging the error.

  • Uncaught promise rejections from a route handler function will be handled for you – When a rejected promise is not caught and handled by your code within a route handler function, the framework will catch it and gracefully handle it in the same way as if an error has been thrown.

  • Bonus points: Return values from route handler functions are sent as a response body – This means you don’t need to explicitly call a method to send a response e.g. response.send(). This is a nice to have though.

What happens when a framework doesn’t natively support async code?

The asynchronous control flow of your application will behave unpredictably, especially when things go wrong i.e. if there’s a promise rejection which you haven’t caught. Because the framework has no awareness of promises, it won’t catch promise rejections for you. If an unhandled promise rejection occurs, it’s most likely that a response will never be sent back to the client. In most cases this will cause the client’s request to hang.

Here is an example route defined in the style supported by some Node.js frameworks:

app.get("/user/:user_id", async (request, response) => {
	const user = await getUser(request.params.user_id);
	response.json(user);
});

If the route above is defined on a server instance created by a framework which doesn’t natively support async code, you’re very likely to run into problems. The route handler function doesn’t implement any error handling code (either intentionally or by accident). If the promise returned by the getUser() function rejects, it will result in an unhandled promise rejection.

Unhandled promise rejections can cause memory leaks in your application. If the memory leak is bad enough, the node process will eventually run out of memory and your application will no longer be able to handle requests.

From Node.js v15.0.0 onwards an unhandled promise rejection will throw an error, causing the node process to exit i.e. your application will crash (in previous versions a warning message would be emitted to stderr).

Which Node.js frameworks natively support async code?

Many Node.js frameworks now support async code, so I think it’s more useful to highlight popular frameworks which don’t support async code:

  • Express – No native support for async code. There’s not been a release of Express for almost 2 years now, so it looks unlikely that this framework will support async code any time soon. If you are unable to move away from Express at the moment, you can monkey patch in support for async / await.
  • Restify – No native support yet. Support merged mid-2020, scheduled for a v9 release.

An excellent alternative to Express or Restify is the Fastify framework. It has full native support for async code and is in active development. There is also a fastify-express plugin available which can help ease your migration path away from Express.

How to write async code safely in Node.js

Build new projects with a framework which natively supports it

If you can, you should absolutely use a Node.js framework which has native support for async code. This should be one of your minimum requirements when choosing a Node.js framework for a new project.

Migrate existing applications to a framework which natively supports it

If your existing Node.js applications are using a framework which doesn’t natively support async code, you should strongly consider migrating them to one which does.

If you’re using Express, the ‘Which Node.js frameworks natively support async code?‘ section in this article has tips on migrating away from it.

Let unhandled promise rejections crash your node process!

This is really important.

You shouldn’t set an event handler for unhandledRejection events, unless the event handler will exit the node process. You risk memory leaks in your application if the node process doesn’t exit when an unhandled promise rejection occurs.

Until you start using Node.js >= v15.0.0, you should use the make-promises-safe module in your Node.js applications. When an unhandled promise rejection occurs in your application, this module "prints the stacktrace and exits the process with an exit code of 1" (i.e. crashes your application).

Learn more

If you want to read more on the subject, the official Learn Node.js website has an excellent page which covers the path from callbacks ➜ promises ➜ async / await in JavaScript: Modern Asynchronous JavaScript with Async and Await.

If you want to learn more about how to use promises correctly in Node.js and avoid some common pitfalls, I recommend watching the talk Broken Promises by James Snell.

Meanwhile, on a less technical note…

Screenshot of a tweet by @simonplend: "Unhandled Rejection is so obviously an emo band waiting to happen. Please make this a thing  
🙏"