DEV Community

Pahan Perera
Pahan Perera

Posted on

How to create a shared context between asynchronous calls in nodejs

👋 Hello Everyone..!!!

As a javascript developer, even though you don't implement your own asynchronous functions very often, you are very likely to need to use them in your everyday project.

Typically there are two/three ways to deal with asynchronous functions.

  1. Callbacks
  2. Promises
  3. Async/Await (i.e Promises)

You can read more about these here.

Problem Statement

When you have a chain of asynchronous calls (callbacks or promises), how do you share a common context between all these calls?

Let's think of the following example.

You are writing a function called getCustomerOrders() that returns customer details along with his/her active orders. Inside that function you have to call asynchronous getCustomer() and asynchronous getOrders() where both of these function needs a customerId from in the Request.

shared context between asynchronous calls

Solution is simple right? 😊

You just extract the customerId from the Request and pass it to both getCustomer() and getOrders() as function parameters.

const getCustomer = async (customerId: string): Promise<Customer> => {
    return fetchCustomerFromApi(customerId);
};
Enter fullscreen mode Exit fullscreen mode

Yes, this is probably the best way that you share context between asynchronous calls. But do you know an alternative way to share context without passing as parameters?

AsyncLocalStorage

AsyncLocalStorage class of async_hooks module is released as part of Node.js 14.

As per the NodeJS official documentation

These classes are used to associate state and propagate it throughout callbacks and promise chains. They allow storing data throughout the lifetime of a web request or any other asynchronous duration. It is similar to thread-local storage in other languages.

In simple terms, this acts as a global variable that scoped to a particular asynchronous execution context.

Let's see AsyncLocalStorage in action

Let's see how we can refactor our getCustomerOrders() example to use AsyncLocalStorage

  1. First, import AsyncLocalStorage from async_hooks module.
import { AsyncLocalStorage } from "async_hooks";
Enter fullscreen mode Exit fullscreen mode
  1. Next, you have to create instance from AsyncLocalStorage representing the data that you are going to share. In this example we are going to store the customerId.
const userAsyncLocalStorage = new AsyncLocalStorage<{ customerId: string }>();
Enter fullscreen mode Exit fullscreen mode
  1. Now, you have to wrap the getCustomerOrders() function using AsyncLocalStorage.run function. This is where all the magic happens. As the first parameter to the run function, you can pass the data that you want to share.
userAsyncLocalStorage.run(
    {
      // customerId is read from the Request
      customerId: "123456789",
    },
    async () => {
      const customer = await getCustomer();
      const orders = await getOrders();
      // write data to Response 
      console.log({
        customer,
        orders,
      });
    }
  );
Enter fullscreen mode Exit fullscreen mode
  1. Finally, inside the getCustomer() and getOrders() you can retrieve customerId as below.
const getCustomer = async () => {
    const { customerId } = userAsyncLocalStorage.getStore();
    return fetchCustomerFromApi(customerId);
}
Enter fullscreen mode Exit fullscreen mode

That is the end of very basic application using AsyncLocalStorage.

Usage

Global state or variables are generally considered bad
as they make testing and debugging a lot harder. Therefor the pattern of using AsyncLocalStorage to share business data across multiple asynchronous calls (like we share customerId) is not recommended.

But AsyncLocalStorage pattern comes in handy when you develop/use APM Tools, which collect performance metrics.

This post explains how you can use AsyncLocalStorage to create a simple logger component.

Also NodeJS Frameworks like adonisjs uses AsyncLocalStorage extensively during the HTTP requests and set the HTTP context as the state.

You can read more about that here.

❤️ Appreciate your feedback and thank you very much for reading...!!

Latest comments (2)

Collapse
 
jwhenry3 profile image
Justin Henry • Edited

If you're going to use application state, you're defeating the purpose of statelessness.
I feel it would be better to use a class instance that stores the data and the methods would be able to call the state from the instance in this case. This way you can keep "application state" within the context of the execution and not globally.

class SomeProcess {
    constructor(protected state) {}

    getCustomer() {
        return getCustomerWithId(this.state.customerId);
    } 
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
pahanperera profile image
Pahan Perera

👋 Hello Henry, Thank you for reading the post and for the comment. 🙇‍♂️

I totally agree. In my opinion your approach is also boils down to the first approach that mentioned in the post and it is the best way out there. AsyncLocalStorage comes handy in certain scenarios and I wanted to provide a sneak-peek into it.