Habilelabs-Logo
Blog

Async Hooks in Node.js- Features and Use Cases

September 9th, 2021 . 7 minutes read
Blog featured image

Node.js 8 version released a new module called async_hooks. It provides an easy-to-use API to track the lifetime of async resources in Node.js applications.

The Node team introduced this module back in 2017. However, it is still in the experimental state.

The asynchronous resources mentioned here are the objects created by Node.JS that have associated callback and can be called multiple times. Some examples of asynchronous resources are Timeouts, Promises, TCPWRAP, Immediates, etc.

We’ll learn more about Async hooks and their use cases in this blog. Let’s get going then-

Why Do We Need Async Hooks?

Async process creates async resources, for example, file read, database read operation, external API call. So naturally async resources keep track of callbacks, once the process completes. But, if we need to track the async resource like what is happening in the middle of an async resource execution, we don’t have any way to do that. 

The typical lifecycle of such an async resource is similar to this:

Life cycle of an async resource

To solve this concern, Nodejs provided async hooks to spy on life cycle of async resources. 

Enough talking, show me the code!

Hello word to async hooks-

In the example above, we have setTimeout is making an async operation. We have created two async hooks init and destroy. When we execute the code, we can see that it is calling init first, and then callsdestroy function. You might be wondering why we are using fs.writeSync to print on console rather than using console.log.

Console.log is async function itself, so this will cause async hooks again, and will cause infinite recursion. So we can not use any async code inside async hooks. We will discuss it later on again in this article. 

Async Hooks APIs

Essentially, the async hooks API provides these five key event functions that we call during different time and instances of the resource’s lifecycle, to track the async resources. 

Key Asynchronous Hooks

You have to specify the event that you want to trigger out of the following while creating an instance.

All the callbacks are optional. Let’s make this statement a little more obvious- It means if the cleanup data of the resource is to be tracked, then you only need to pass the destroy callback.

init

The init callback is called whenever a call is created that has the possibility of triggering an asynchronous event. Just for the record, at this point, we’ve already associated the hook with an async resource.

init callback receives these parameters when called-

  • asyncId: Each async resource, when identified, gets a unique ID.
  • type: It depicts the type of the async resource in string form that triggered init callback.
  • triggerAsyncId: asyncId of the resource for whose context the async resource was created. It shows the reason behind creating a specific resource.
  • resource: It represents the reference to the async operation releasing during destroy.

All the other callbacks get only one parameter- asyncId.

Before

Whenever an async operation initiates or completes, a callback notifies the user of its status. The before callback is called right before this mentioned callback is executed, and the relevant resource will be assigned with a unique asyncId identifier.

The before callback can be called any times from 0 to n. For instance, when the asynchronous operation gets cancelled, it will be called 0 times, on the other hand, the persistent type resources can call it multiple times.

After

Similar to before, it is called post-execution of the relevant asynchronous resource or right after when the specified callback in before callback finishes its execution.

Destroy

It’s quite easy to guess, isn’t it? Yes, you guessed it right! 

It is called every time the asynchronous resource, corresponding to the unique asyncId, is destroyed regardless of whatever happened to its callback function.

However, in some cases, the resource depend upon the garbage collection for its cleanup which might cause a memory leak in the application, which results in avoid calling destroy. If the resource does not rely upon the garbage, then it won’t be a problem and destroy will do the cleaning.

promiseResolve

This callback is triggered when the Promise gets its resolve function, which is invoked either directly or by some external means to resolve a promise.

Promise Resolve

Use Cases of Async Hooks

Listed below are some major features and use cases of async hooks-

– Promise Execution Tracking

Async hooks play a vital role tracking the lifecycle of promises and their execution, as promises are also asynchronous resources. 

Whenever a new promise is created, the init callback runs. The before and after hooks run pre- and post-completion of a PromiseReactionJob and the resolve hook is called when a promise is resolved.

– Web Request Context Handling

Another major use case of Async Hooks is to store relevant information in context to the request during its lifetime. This feature becomes highly useful to track the user’s behavior in a server.

With async hooks, you can easily store the context data and access it anywhere and anytime in the code.

The process starts with a new request to the server, which initiates calling createRequestContext function to get data to store in a Map. Every async operation initiated as part of this request will be saved in the Map along with the context data (Here init plays an important part). 

Next, destroy keeps the size of the Map under control and do the cleaning. This is how you can get the current execution context by calling getContextData anytime.

– Error Handling with Async Hooks

Whenever an async hook callback throws an error, the application follows the stack trace and exits due to an uncaught exception. Unless you run the application with-abort-on-uncaught-exception, which will print the stack trace exiting the application, the exit callbacks will still be called.

The error handling behavior is due to the fact that all these callbacks run at potentially unstable points, for instance during construction or destruction of a class. This process prevents any unintentional and potential abort to the process by closing it quickly.

– Printing in Async Hooks

Printing is an asynchronous operation and console.log() triggers calling async callbacks. However, using such asynchronous operations inside async callbacks causes infinite recursion. 

For instance, when init runs, the console.log() will trigger another init callback causing endless recursion and increasing stack size.

To avoid this, you should use a synchronous operation like fs.writeFileSync(file, msg, flag) while debugging as it will print to the file without invoking AsyncHooks recursively.

If the logging requires an asynchronous operation, you can track what caused the async operation to initiate using AsyncHooks. As it was logging itself that invoked the AsyncHooks callback, you can skip it which will break the infinite recursion cycle.

– Enhanced Stack Traces

The creator of Node.js, Ryan Dahl, talked about debugging problems in node.js due to event loops, which kills the stack trace. As the async hooks facilitate better tracing of async resources, it allows developers to improve and enrich the stack trace.

Bottom Line

Do you know that you can also measure the duration of an asynchronous operation in real-time? Yes, you can, but by integrating Async hooks module with Performance API module.

Well, this was all about async hooks. I would suggest you to refer to this link if you want to learn more about async hooks.

Thanks for reading!!

Don’t forget to share your response with us in the comment section!

Author: payal
Share: