Node.js is one of the most popular programming languages used on the web nowadays. It’s single core design has lots of positives, but sometimes can be a little challenging – especially when it comes to passing the context between asynchronous invocations. It seems not to be such a problem anymore, though. Developers can use AsyncLocalStorage in Node.js to overcome long lasting problems. Let’s dive in to check how to do that.

What problems can AsyncLocalStorage in Node.js solve

I bet that most of you have written a custom HTTP request logger to be sure what’s happening behind the scenes for each server invocation. The code roughly could look like this.

const express = require("express");

const { v4: uuid } = require("uuid");

const indexRouter = require("./routes/index");

const app = express();

function requestLogger(req, ...args) {
  console.log(req.id, args);
}

app.use(express.json());
app.use(express.urlencoded({ extended: false }));

app.use((req, res, next) => {
  req.id = uuid();
  requestLogger(req, "request started");
  next();
});

app.use("/", indexRouter);

app.use((req, res, next) => {
  requestLogger(req, "request ended");
  next();
});

module.exports = app;

Let’s take a look at the requestLogger function: the first parameter includes a request object, while the second one is the message that shall be logged. By looking at this piece of code, it is easy to identify the problem – it is needed to pass the “req” variable (lets call it the request context) to the logger. This kind of a pattern has been known in the Node.js community for years, but obviously that does not mean it should be the standard. It’s worth pointing out that it’s a relatively simple example – oftentimes you may need to log for details of SQL query or details of cloud-related operations (for example by uploading to AWS S3). The situation can get complex really fast and it’s hard to code just one logging method for each use-case. But the solution is on the horizon.

AsyncLocalStorage in Node.js to the rescue!

AsyncLocalStorage is a class which creates an asynchronous state within callbacks and promise chains. It allows us to persist context through a lifetime of async operations, for example within a web request, RabbitMQ event or WebSocket message.

Before I start explaining how to do it – make sure you are running Node.js in versions 12.17.0 or 13.10.0 at least (14+ supports AsyncLocalStorage by default).

Let’s try to refactor the previous example so it uses this new feature. Also, to show the power of AsyncLocalStorage in Node.js, we will make our logger inform us about the path of request and time delta between the request start and log call. 

const express = require("express");

const { AsyncLocalStorage } = require("async_hooks");
const { v4: uuid } = require("uuid");

const indexRouter = require("./routes/index");

const app = express();
const context = new AsyncLocalStorage();

function requestLogger(...args) {
  const store = context.getStore();
  const id = store.get("id");
  const timeStart = store.get("timeStart");
  const { originalUrl } = store.get("request");
  console.log(`${id}, ${originalUrl}, ${+new Date() - timeStart}ms`, args);
}

app.use(express.json());
app.use(express.urlencoded({ extended: false }));

app.use((req, res, next) => {
  const store = new Map();
  context.run(store, () => {
    store.set("id", uuid());
    store.set("timeStart", +new Date());
    store.set("request", req);
    requestLogger("request started");
    next();
  });
});

app.use("/", indexRouter);

app.use((req, res, next) => {
  requestLogger("request ended");
  next();
});

module.exports = app;

First, it’s necessary to import and create the AsyncLocalStorage context. Then let’s take a look at our middleware on line 22 – first Map is created. It’s going to keep our context. This way we can store more than one variable inside our context with flexible data structure. By calling context.run with our store as an argument, a shared context instance is being created. Inside the callback, we supply the store with needed details about our context – here request ID is being created, request object saved and the timestamp of invocation is saved. 

Now, let’s move back to the requestLogger function. First of all, we can remove all context-related arguments. Later, we have to grab our store from the AsyncLocalStorage instance. The latter is just a simple implementation of the business logic. As you can see, the solution is robust and easy to read. Moreover, we can store as many context-related details as we want. 

The versatility of AsyncLocalStorage in Node.js

Example shown in this article is really easy – but this is just an explanation. You can easily implement sophisticated monitoring tools, loggers with lots’ of features, much better error handling, single SQL transaction per HTTP request and more. 

But there are also some tips you have to remember about:

  • Due to performance, you shouldn’t really create more than 10-15 AsyncLocalStorages. Also, library creators shouldn’t really use it.
  • If you are not really sure whether data will be cleared via GarbageCollector, use the context.exit() method.
  • Remember about the limitations of async/await – asynchronous operations which take less than 2ms shouldn’t really we wrapped with Promises.

With these in mind I hope that you can benefit from this new language feature and clean up your code a little bit!

Based on Node.js documentation and “Request context tracking (AsyncLocalStorage use cases and best practices)“ talk by Vladimir de Turckheim, Lead Node.js Engineer @ Sqreen.

If you find this interesting, be sure to check out other posts on our blog, where we cover topics such as service workers in progressive web apps and break down the details of a solid software development process!