In a previous post, we discussed how Node.js is a single-threaded environment and still manages to serve multiple requests simultaneously: Node.js event loop: concurrent processing in a single-threaded environment.
This is a good way to avoid the problems that come along with multi-threading like race conditions. But this extra peace of mind comes at a price. Now that you do not have a thread-per-request architecture, you lose the ability to store your execution context in thread-local variables.
For example, you might want the session data (say userid) to be available everywhere in your code so that you can use it whenever you want. One obvious way to achieve this would be to pass a context object to all of the functions, that you invoke, as a parameter. But, I don’t have to tell you that this makes your code dirty and unscalable.
What you actually are thinking is: “I wish I had a place that is exclusive to the API request where I could store my context”. As we discussed, multi-threaded environments like Java gives you thread-local variable to solve this problem. Because the thread itself is exclusive to the request.
Node.js gives you async_hooks instead.
But before we get into the async_hooks, as usual, let’s do a thought experiment to understand what we need.
The thought experiment
As I said, in Node.js, you can just forget about associating the request context with the thread as all of the requests are served by one single main thread.
BUT, what if
- each Node.js function could keep one unique context object exclusive to its current execution (which means it automatically gets created when the function starts execution each time and is private to that execution. When the function gets executed again, a new context object is created) and
- there was a way for any function to access the context of the function that invoked it?
See the diagram below

The code below might help to understand it better. Note that I am INVENTING two functions getParentContext and getCurrentContext for the convenience of explaining the logic.
/**
* Import our invented functions
*/
const getParentContext = require('parent-context');
const getCurrentContext = require('current-context');
// now, to the code we have to write
async function a(userId) {
const currContext = getCurrentContext();
currContext.userId = userId;
return await b();
}
async function b() {
const parentContext = getParentContext();
const userId = parentContext.userId;
const currContext = getCurrentContext();
currContext.userId = userId;
return await c();
}
async function c() {
const parentContext = getParentContext();
const userId = parentContext.userId;
// do something with the userId.
}
Now, we have an implementation to pass the userId to the function c() without passing it as a function parameter.
This implementation has a problem that we have to deal with the userId in the function b() even though it doesn’t really need it.
We can solve it by modifying the code to add a direct reference to the parent’s context in the child context. As follows.

/**
* Import our invented functions
*/
const getParentContext = require('parent-context');
const getCurrentContext = require('current-context');
// now, to the code we have to write
async function a(userId) {
const currContext = getCurrentContext();
currContext.userId = userId;
return await b();
}
async function b() {
const parentContext = getParentContext();
const currContext = getCurrentContext();
currContext.parentContext = parentContext;
return await c();
}
async function c() {
const parentContext = getParentContext();
const userId = parentContext.parentContext.userId;
// do something with the userId.
}
We just formed a linked list of contexts.
Now, we are not dealing with the userId in the function b(). However, we made the function c() dirty as now it needs to know that it has to go back exactly twice in the parent hierarchy to get the userId.
If you think about it, these functions don’t really need a context object of their own. The idea of the context object is to make the common values accessible throughout the execution of the request. Then why not create one common context object that is “exclusive to the request” and let all the functions refer to this one context object? Look at the diagram given below.

And the code would look as follows.
/**
* Import our invented function
*/
const getCurrentContext = require('current-context');
// now, to the code we have to write
async function a(userId) {
const currContext = getCurrentContext();
currContext.userId = userId;
return await b();
}
async function b() {
return await c();
}
async function c() {
const currContext = getCurrentContext();
const userId = currContext.userId;
// do something with the userId.
}
I know! We just arrived at a thread-local-like solution. But unfortunately, we can’t have that in Node.js. Well, at least not directly. Because the two assumptions that we made at the beginning of this thought experiment do not exist.
So, how do we do it?
async_hooks comes to the rescue…
Async_hook
Async hooks provide a way to keep track of the lifetime of the asynchronous resources created in Node.js.
In our thought experiment, we kept track of the lifetime of a task going through a series of function calls. If you remember from my previous post (here), the Node.js main thread keeps executing one task until the task goes to an asynchronous operation. When an asynchronous operation is started (eg: a DB query), it hands this task over to the asynchronous helper libraries and starts executing the next task in the event loop. This exactly is the moment we lose track of our context; because the main thread is with another task now.
What I am trying to say is that, in order to keep track of the lifetime of a task(or an http request) we don’t need to keep track of each function call. They will run continuously on the main thread till an async operation is started.
Instead, keep track of these asynchronous operations flow and keep one common context object for all of them like we did for the functions in the thought experiment. Don’t worry if that statement is confusing. We will see some code.
But, how does async_hooks solve this problem?
- async_hooks assigns a unique id to each async resource
- and it also keeps track of the parent async resource
There are a few hook functions that you can register with async_hooks that will get invoked whenever a new async resource is created and the hook functions have parameters that specify the id of the generated async resource and the id of the async resource that created it (See, you got the connection to the parent). With this, you can create a context mapping along the execution of these async resources and refer to the context anywhere from the execution.
I know it is a bit confusing. Let’s see some code.
Usage of async_hooks starts with registering the hook functions and enabling it.
// register callback functions
const asyncHook = async_hooks.createHook({ init, before, after, destroy, promiseResolve });
// then enable the hooks
asyncHook.enable();
We are interested in the callback functions init and destroy.
init gets invoked when a new async resource is created and destroy gets invoked when it is destroyed. We will use init function to associate our context with the resource and destroy function to do the clean-up.
init function has the following arguments
asyncId— A unique ID for the async resourcetriggerId— TheasyncIdof the resource which created this resource (the parent)type— The type ofresource. All types are available hereresource— The async resource itself
Now, let’s implement init and destroy functions to manage the context
// context-store.js
const contexts = new Map()
const init = (asyncId, type, triggerId) => {
/*
* if the parent already has a context,
* then it is the context of this child too.
* It will be reused by its children as well.
* Note that we are not creating a new context
* if parent context is not present. This is
* intentional. Because, init gets invoked
* for every async resource creation after the invokation
* of async_hooks.enable().
* We don't need all of them. So we deligate the creation
* of the context for the parent to the application
* logic, so that, it can decide where to start the
* context tracking for a particular request.
*/
const ctx = contexts.get(triggerId);
if (ctx) {
contexts.set(asyncId, ctx);
}
}
const destroy = (asyncId) => {
/*
* we no longer need to keep the reference
* of this async resource in the contexts object.
* Deleting it.
*/
contexts.delete(asyncId);
}
/*
* and finally, export these to be used outside
*/
module.exports = {
contexts,
init,
destroy
}
Great! we have context storage now. But how do I use it in my code? I need the asyncId of the current function to get the context associated with it. Where do I get it?
Again, async_hooks has got your back. It exposes a function named executionAsyncId() to get the asyncId of the current execution context.
Now, let’s see all these things working together in a web server that has an odd functionality of returning the user details if google.com is accessible.
// index.js
const {
contexts,
init,
destroy
} = require('./context-store')
const async_hooks = require('async_hooks')
const ah = async_hooks.createHook({ init, destroy });
ah.enable();
const ProcessRequest = require('./process-request')
const http = require('http');
http.createServer(function (req, res) {
// This line returns the async id of this function call; say 1
const currentAsyncId = async_hooks.executionAsyncId();
// creating the context here since we need the context tracking to start here.
contexts.set(currentAsyncId, {});
const userid = req.headers.userid;
contexts.get(currentAsyncId)['userid'] = userid;
new ProcessRequest().returnUserIfGoogleIsAccessible()
.then(user => {
res.json({ user })
})
.catch(error => {
res.json({ error });
});
}).listen(8080);
Given below is the code for the ProcessRequest class
// process-request.js
const db = require('./db')
const { contexts } = require('./context-store')
const { invokeHttpEndpoint } = require('some-http-client')
class ProcessRequest {
returnUserIfGoogleIsAccessible() {
return new Promise((resolve, reject) => {
invokeHttpEndpoint('https://google.com')
.then(g => {
this.getCurrentUser()
.then(u => resolve(u))
.error(e => reject(e));
})
.catch(e => {
reject(e);
});
}
}
getCurrentUser() {
/*
* Here we call executionAsyncId() again
* this time you would get an id
* that is different from the id that you got
* in index.js file (say 10). But, our init hook
* would have already linked these ids to
* the same context object when this async
* resource (ProcessRequest object) was created
* in index.js
*/
const currentAsyncId = async_hooks.executionAsyncId();
const userId = contexts.get(currentAsyncId)['userid'];
return db.getUserDetails(userId);
}
}
module.exports = ProcessRequest;
Note that we invoked async_hooks.executionAsyncId() two times in the code snippet above. Once inside the index.js file and once in the process-request.js file. Each of these times, the function would have returned two different ids because both of these happened in different async resources. But our init would have already done the context chaining when the ProcessRequest object was created and the contexts.get(currentAsyncId) call would return the same context object both of these times for any given request.
So you kept track of the userId efficiently without passing it as a function argument. Clean and scalable code
I hope you got a glimpse of how powerful async_hooks are.
The popular http context management library express-http-context uses async_hooks in a much better way with namespaces. If you would like to read more about async_hooks, the official documentation is a good place to go.
Happy coding!