Async Hooks — A whole new world of opportunities

illustration by Derick Bailey

Lately Node foundation released, as part of Node.js 9, a new and interesting feature called Async Hooks.

It’s important to say that as I’m writing this, the feature is considered experimental and isn’t recommended for a production usage, but we as developers — are curious beings, and probably more than the average human. We’re not afraid of some experimenting.

Because it’s such a new feature, there’s not a lot of information about it, but a good place to start is by reading the documentation and a blog post by Irina Shestak.

After I was done with the above light reading, I was ready to dive in deep to solve a personal problem I had — managing a request context.

The problem is quiet simple and known. While my team and I started to monitor our Node app, our main pain point was to group the logs by requests. The common solution is to add a transaction id to each log row. Once you have a transaction id you can filter your log by requests and even send it to your client in case of an error.

This pattern is pretty easy on synchronized platforms such as Ruby on rails and Python Django. Each request is handled by a thread. Every thing is synchronized so you can save any request context on your thread context.

On the other hand, on a platform like Node, this becomes quiet challenging.

Why?

As we all know to declaim, Node is an event-driven and single-threaded platform. This means we have no thread context we can relay on, since we only have one thread.

So what can we do about it?

We can pass a context object to all functions but this doesn’t scale well and is very difficult to maintain in large codebases.

We found a better solution — node-continuation-local-storage. To use the package we need to create a namespace. Inside our namespace we can get and set variables easily. But sometimes, mainly when we are dealing with external packages we found the package unreliable and context was lost.


Async Hooks module provides an API for tracking the lifetime of asynchronous resources created inside Node.js.

If you are not familiar with asynchronous resources, it is better to start with the famous YouTube about Node’s event loop.

How can we use it to maintain a request context like we want? Let’s take a brief look on the API. Using async hooks is quiet easy:

For our purpose we will use init and destroy .

Each time an asynchronous resource is created, init function is called with an asyncId , type , triggerId and resource .

  • asyncId — A unique ID for the async resource,
  • triggerId — The asyncId of the async resource in whose execution context this async resource was created, or in simple words, the asyncId who caused this.
  • type — The type of resource . All types are available here
  • resource — The async resource itself

As you can see, when keeping track of the triggerId for each async resource, we can link it to the actual context it was created with!

The last brick we are missing is a way to start each request chain.

Scrolling down the API will introduce us async_hooks.executionAsyncId() which is exactly what we need. executionAsyncId is the asyncId of the current execution context.

Let’s sum it all by the following example:

Here’s the output:

When we are running our program, execution is 1. This is why the triggerId for Timeout and TIMERWRAP is 1 as well.

When timeout is past and Node runs our callback, the execution is6 , which is the asyncId of Timeout resource. At the end we can see the garbage collector destroying our async resources and we are done.


Now that we have it all, Let’s connect the dots together. You can find all the following code in an NPM package I’ve created called node-request-context.

Firstly, we want to have an API to declare the start of our async callback chain. In other words — when should we create our context. We’re gonna try to have the API as close to node-continuation-local-storage as possible.

Second step, we will create Namespace object and keep it in a namespaces global object

Third step, and the most existing one, combining async_hooks and implementing createHooks function!

init will be used to track our context by tracking triggerId and asyncId . destroy, on the other hand, will be used to clean our context object.

On init we can watch how we keep track the request using the new capabilities of async_hooks.


Our package is done! now let’s use it in our Node app!

For our example, we will have someResource with async callback and we want to maintain a transactionId for each request.

and here is our someResource

That’s it! we are done. We can set and get any variable we want during a request.

As I’ve mentioned before, I’ve created a NPM package with all the code and called it node-request-context. You can clone it, run npm run example and watch it all in action.

Enjoy!

P.S — Developers in Israel? Autodesk TLV is looking for you. Apply here.