Skip to content PaweΕ‚ Grzybek

Avoid parameter drilling by using the Node.js AsyncLocalStorage

Although considered a bad practice, global variables are probably the easiest form of sharing a context. In this example, I define the context global variable and use it to share the uuid value between the main and logger functions:

import crypto from "node:crypto";

const context = {};

function logger(msg) {
  console.log(`${context.uuid}: ${msg}`);
}

function main() {
  const uuid = crypto.randomUUID();

  context.uuid = uuid;

  logger("Hey there πŸ‘‹");
}

main();
// 239252e8-a5de-4243-87ce-87927355bae2: Hey there πŸ‘‹

This approach works OK for synchronous code, but it is problematic when we introduce asynchronous operations. Let me illustrate this issue by adding a http server into the mix:

// πŸ‘Ž do not blindly copy this code, it is bad example

import crypto from "node:crypto";
import http from "node:http";

const context = {};

function logger(msg) {
  console.log(`${context.uuid}: ${msg}`);
}

http
  .createServer((req, res) => {
    const uuid = crypto.randomUUID();

    context.uuid = uuid;

    // In real-life scenarios, this is the place where you would define some asynchronous operations that take some time to complete.

    logger("Hey there πŸ‘‹");
    res.end();
  })
  .listen(8080);

Every consequent request will update the global context, and the logger function will always log the last value of uuid, not necessarily the one correlated with the request. Passing the uuid as a parameter to the logger (parameter drilling) or sticking it to the req object are common solutions for this problem. Both of them can quickly become cumbersome, though.

Node.js AsyncLocalStorage

Available since Node.js 13.10, the AsyncLocalStorage class provides a way to store context-specific data throughout the lifetime of a web request or any other asynchronous operation. It is similar to the thread-local storage (TLS) concept used in different programming languages. Let’s take advantage of this API and rewrite our example:

// πŸ‘ yeah, you can copy this one, it is good

import crypto from "node:crypto";
import http from "node:http";
import { AsyncLocalStorage } from "node:async_hooks";

const context = new AsyncLocalStorage();

function logger(msg) {
  console.log(`${context.getStore()}: ${msg}`);
}

http
  .createServer((req, res) => {
    const uuid = crypto.randomUUID();
    context.run(uuid, () => {

      // In real-life scenarios, this is the place where you would define some asynchronous operations that take some time to complete.

      logger("Hey there πŸ‘‹");
      res.end();
    });
  })
  .listen(8080);

Now, all consequent requests will log the correct uuid value without the risk of interfering with each other. Correlation IDs are just one of the many use cases for the AsyncLocalStorage API. It can be used for storing the current user, transaction ID, reporting data and whatnot.

Broad uses cases of the Node.js AsyncLocalStorage API inspired community members to propose an Async Context for JavaScript. My fingers are crossed for this proposal to quickly become a part of the ECMAScript standard.

Thanks to Syntax.fm

One of the recent episodes of Syntax.fm inspired me to explore the subject and write this article. Thanks, Wes and Scott, for the great content you are creating!

Leave a comment

πŸ‘† you can use Markdown here

Your comment is awaiting moderation. Thanks!