Asynchronous applications are great because they are faster compared to synchronous applications. However, they create a new challenge when debugging because it is difficult to inspect log and tracking requests of a single threaded architecture such as Node. In Node, there is a tool to help with this called Async Hooks. We will explore that tool in this article.
Fundamentally, Node.js is a single-threaded, non-blocking architecture. This terminology simply means that Node.js can attend to other requests while waiting for a previous request; for instance, it can read a file from a disk. This is also referred to as concurrent action, as the operations occur simultaneously, resulting in increased performance.
Audience
This article is for developers who have basic understanding of Node.js.
The Problem: Monitoring Log Requests
As highlighted above, Node.js applications are asynchronous by nature. The disadvantage of having asynchronous applications is that it is difficult to keep log requests of multiple asynchronous resources running on a single thread. Synchronous applications can have multiple threads where it is easier to inspect and track log requests. Async applications, sadly, don’t have the same benefit.
Since it is difficult to inspect log requests in Node.js applications, debugging becomes more difficult, too. In an attempt to solve the challenge of inspecting log requests and consequently easing the pain of debugging a piece of code, AsyncHooks were introduced.
A brief History
AsyncHooks were first introduced in Node.js version 8 (in 2017) and in the current version (v14), AsyncHooks is still experimental. When a feature is experimental, it means it is not deemed fit for production use.
What are Async Hooks
Async Hooks is an Application Programming Interface (API) that enables tracking of asynchronous resources’ lifetime. In other words, Async Hooks enable us to listen to events occurring in asynchronous resources. Any resource with a callback is an asynchronous resource; for instance, Timeouts and Immediate.
Debug in Node.js with Async Hooks
The Async Hooks utility provides asynchronous resource tracing capabilities to measure the performance of your application. This makes it easy to debug. When handling errors, asynchronous resources associated with Async Hooks display the stack trace and terminate the process each time an error is thrown. This is because they operate at critical stages of an object’s event lifetime; for instance, class construction and destruction. In addition to that, as a result of using Async Hooks, there is minimum performance overhead.
Async Hooks Handles Leaks in Node.js v14
Besides providing an easy way for tracking async resources, Async Hooks are capable of handling memory leaks. A Node.js application is said to have had memory leaks when the memory and CPU usage keeps rising for no clear reason during the period the callback is in state, resulting in poor performance. In the recent updates of Node.js version 14, by using Async Hooks, async storage methods can no longer leak to the outer context.
Accessing the Async Hooks
So far we have defined Async Hooks, the problem it solves, alongside its benefits. Let’s now look at how to access Async Hooks module for tracking asynchronous resources.
const async_hooks = require('async_hooks')
Register functions
Once the Async Hooks are added as a module, as shown above, we can use the register functions. Let’s take a look at the register functions that make it possible to track an asynchronous resource at different event lifetime.
-
Instantiation
init(asyncId, type, triggerAsyncId, resource)
The instantiation occurs when an asynchronous resource is created with chances of creating an asynchronous event. This means that this callback will be called once during creation. The init
function takes four parameters as shown above.
asyncId
: The first parameter, asyncId, is the unique id of the async resource. This asyncId provides information on when the resource was created. Using executionAsyncId()
method, one can save the asyncId
in myId variable as shown below:
const myId = async_hooks.executionAsyncId()
type
: The type of any resource. Microtask, Timeout, Immediate, TickObject are instances of asynchronous resource types. The information displayed by one resource is different from other resources as they are all unique. You can also define your own type of resource besides the one Node.js provides using public embedder API.
triggerAsyncId
: The unique ID of the async resource in whose execution context this async resource was created. The triggerAsyncId gives us detailed information about the resource. The code below shows how you can access the triggerAsyncId:
const tid = async_hooks.triggerAsyncId()
resource
: The async resource that has been initialized.
2. Before before(asyncId)
The Before callback before(asyncId)
takes the asyncId
as a parameter. Let’s look at an example of reading data from a disk asynchronously. In this example, the before(asyncId)
callback is called first before the asynchronous operation of writing data to a disk. Depending on the type of resource, the before callback
can be called more than once.
3. After after(asyncId)
The after callback is executed after the user callback (i.e asynchronously reading data from a disk) is done executing. In a scenario where the user callback ends up throwing an error, the error comes first before the after callback
. The after callback is usually called once.
4. Destroy destroy(asyncId)
This callback is called when the resource with the asyncId is destroyed
. This callback is called once when the resource is destroyed.
Enabling and Disabling Asynchronous Instance
const async_hooks = require('async_hooks'); const hook = async_hooks.createHook(callbacks).enable()
Now that we understand the register functions to call at different lifetime events of an asynchronous resource, let’s explore the two most important methods that can start or end the resource tracking. To start tracking our asynchronous resources, we use .enable()
method soon after asynchronous instance creation as shown above. To stop tracking the asynchronous resource, we use the disable .disable()
method.
Console.log() is not needed in Async Hooks
When you want to print in Async Hooks, console.log()
will let you down. It will throw maximum call stack size exceeded
error because console log is an asynchronous resource, and using that will just add to the stack the asynchronous resources to be tracked.
Let’s have a look at the overview of the async hooks API snippet from Node.js documentation:
const async_hooks = require('async_hooks'); // Return the ID of the current execution context. const eid = async_hooks.executionAsyncId(); // Return the ID of the handle responsible for triggering the callback of the // current execution scope to call. const tid = async_hooks.triggerAsyncId(); // Create a new AsyncHook instance. All of these callbacks are optional. const asyncHook = async_hooks.createHook({ init, before, after, destroy}); // Allow callbacks of this AsyncHook instance to call. This is not an implicit // action after running the constructor, and must be explicitly run to begin // executing callbacks. asyncHook.enable(); // Disable listening for new asynchronous events. asyncHook.disable(); function init(asyncId, type, triggerAsyncId, resource) { //code}
Async Hooks, in Summary
Let’s recap everything, we discussed above:
- Async Hooks allow us to track asynchronous resources in a Node.js application.
- They prevent async storage methods from leaking to outer context.
- There are four main events in the lifetime of any asynchronous resource: during initiation, before, after callback and destroying of the asynchronous operation.
- Never forget to enable your asynchronous instance for asynchronous resources you are tracking.
The post Debug in Node.js with Async Hooks appeared first on Sweetcode.io.