Async Hooks are a new Node8 feature that provides an API for tracking the life cycle of asynchronous resources in NodeJS. They are built into Node modules and can be referenced directly:

let asycnHooks = require('async_hooks');

The reason why the async_hooks module is introduced is that it is difficult to properly track the handling logic and relationships of asynchronous calls during asynchronous calls. The async_hooks module provides a friendly solution to this problem by providing the following functions and features:

  • Each function provides a context, which we call the Async Scope;
  • Each async scope has an asyncID, which is the flag of the current async scope. The asyncID in the same async-scope must be the same. The outermost asyncID is 1. Each asynchronous resource is incremped asyncID at creation time.
  • Each async scope contains a triggerAsyncID to indicate that the current function was triggered by that async scope.
  • With AsyncID and TriggerAsyncID we can easily trace the entire asynchronous invocation relationship and link.
  • We can async_hooks. CreateHook function to register for each asynchronous resource in the life cycle of the init/before/after/destory/promiseResolve related event monitoring function;
  • The same async scope may be called and executed several times. No matter how many times it is executed, its asyncID must be the same. Through listening function, we can easily track the number and time of its execution as well as the relationship between the text.

    ExecutionAsyncId and triggerAsyncId

    The async_hooks module provides the executionAsyncid and triggerAsyncid functions to get the asyncId and triggerAsyncid for the current context:

    const async_hooks = require('async_hooks');
    const fs = require('fs');
    console.log('global.asyncId:', async_hooks.executionAsyncId());  // global.asyncId: 1
    console.log('global.triggerAsyncId:', async_hooks.triggerAsyncId()); // global.triggerAsyncId: 0
    fs.open('./app.js', 'r', (err, fd) => {
      console.log('fs.open.asyncId:', async_hooks.executionAsyncId()); // fs.open.asyncId: 7
      console.log('fs.open.triggerAsyncId:', async_hooks.triggerAsyncId()); // fs.open.triggerAsyncId: 1
    });

    You can see that the global asyncID value is 1, the fs.open callback value is 7, and the triggerAsyncID value is 1. You can see that the fs.open asynchronous resource is triggered by the global incoming call.

async_hooks.createHook(callbacks)

We can use async_hooks. CreateHook to create a hook for an asynchronous resource, and register callbacks to the async_hooks. CreateHook as input to the async_hooks. These hook functions are fired whenever an asynchronous resource is created/executed/destroyed:

const async_hooks = require('async_hooks'); const asyncHook = async_hooks.createHook({ init(asyncId, type, triggerAsyncId, resource) { }, destroy(asyncId) { } }); asyncHook.enable(); // enable the hook function with the enable function

Currently the CreateHook function can accept five types of Hook Callbacks as follows:

init(asyncId, type, triggerAsyncId, resource)

The init callback is typically fired when an asynchronous resource is initialized. Its parameters are explained as follows:

  • AsyncId: Each asynchronous resource generates a unique flag
  • Type: The type of an asynchronous resource, which is usually the name of the resource’s constructor. You can refer to the async_hooks official documentation for these types
  • TriggerAsyncID: Represents the AsyncID that triggers the corresponding Async Scope to which the current asynchronous resource is created
  • Resource: Represents the initialized asynchronous resource object
  • Before callbacks are typically invoked after the asyncID corresponding asynchronous resource operation has completed and before the callback is ready to be executed. Before callbacks may be executed several times, depending on the number of callbacks.

after(asyncId)

After callbacks are usually called immediately after the asynchronous resource executes the callback. If an uncaughtException occurs during the execution of the callback, the AFTER event will be called after the “uncaughtException” event is emitted.

destroy(asyncId)

The asyncId event is called when the corresponding asynchronous resource is destroyed. Some asynchronous resource destruction relies on the garbage collection mechanism, so in some cases the deStory event may never be fired due to a memory leak.

promiseResolve(asyncId)

When the Resovle function in the Promise constructor is executed, the promiseSolve event is emitted. In some cases, some resolve functions are executed implicitly, such as when the.then function returns a new Promise, which is also called.

The following expression is used to fire two promiseSolve events, the first being the resolve function that is explicitly executed in the new Promise() and the second being the resolve function that is implicitly executed in the callback to the.then function

new Promise((resolve) => resolve(true)).then((a) => {});

Promise execution tracing

Because V8’s Promise Introspection API has a high execution cost for fetching asyncId, we do not assign new asyncId to Promise by default. By default, if we use Promises or Async /await, we will not get the correct asyncID and triggerID in the current context. But never mind, we can force the async_hooks to be turned on by executing the async_hooks.createhook (callbacks).enable() function:

const ah = require('async_hooks'); ah.createHook({ init() {} }).enable(); Then (() => {console.log(' asyncId ${aH.executionasyncid ())} triggerId ${ah.triggerAsyncId()}`); });

Note that hook functions for before and after events are fired only when a Promise is chained to a call. That is, they are fired only when a Promise is generated in the.then/.catch function. Otherwise, only the init and promiseSolve hook event functions are emitted.

So when we perform how:

new Promise((resolve) => resolve(true)).then((a) => {});

The flow of events that are fired is as follows (assuming the asyncID for each Promise context is 5, 6, respectively) :

AsyncId = 5 (promiseSolve) asyncId = 5 (init) asyncId = 6 (init) asyncId = 6 (before AsyncId = 6 (promiseLesovle); asyncId = 6 (after)

Exception handling

If an exception occurs in one of the Asynchook callbacks, the service will print the error log and exit immediately, all ‘uncaughtException’ listeners will be removed, and the ‘exit’ event will be emitted. The reason for the immediate exit is that if the AsyncHook function is not stable, it is likely to throw an exception when the next same event is fired. The purpose of these functions is to listen for asynchronous events and should be detected and corrected.

Log print

Since console.log is also an asynchronous call, if we call console.log again in the asyncHook function it will fire the corresponding hook event again, resulting in an endless loop. So we must use synchronous print log tracing in asyncHook, which we can use with fs.writeSync:

async_hooks.createHook({
  init(asyncId, type, triggerAsyncId) {
    const eid = async_hooks.executionAsyncId();
    fs.writeSync(
      1, `${type}(${asyncId}): trigger: ${triggerAsyncId} execution: ${eid}\n`);
  }
}).enable();

reference

Node.js v8.x async Hook is a new feature