Async hooks#

Stability: 1 - Experimental. Please migrate away from this API, if you can.We do not recommend using thecreateHook,AsyncHook, andexecutionAsyncResource APIs as they have usability issues, safety risks,and performance implications. Async context tracking use cases are betterserved by the stableAsyncLocalStorage API. If you have a use case forcreateHook,AsyncHook, orexecutionAsyncResource beyond the contexttracking need solved byAsyncLocalStorage or diagnostics data currentlyprovided byDiagnostics Channel, please open an issue athttps://github.com/nodejs/node/issues describing your use case so we cancreate a more purpose-focused API.

Source Code:lib/async_hooks.js

We strongly discourage the use of theasync_hooks API.Other APIs that can cover most of its use cases include:

Thenode:async_hooks module provides an API to track asynchronous resources.It can be accessed using:

import async_hooksfrom'node:async_hooks';const async_hooks =require('node:async_hooks');

Terminology#

An asynchronous resource represents an object with an associated callback.This callback may be called multiple times, such as the'connection'event innet.createServer(), or just a single time like infs.open().A resource can also be closed before the callback is called.AsyncHook doesnot explicitly distinguish between these different cases but will represent themas the abstract concept that is a resource.

IfWorkers are used, each thread has an independentasync_hooksinterface, and each thread will use a new set of async IDs.

Overview#

Following is a simple overview of the public API.

import async_hooksfrom'node: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, promiseResolve });// 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();//// The following are the callbacks that can be passed to createHook().//// init() is called during object construction. The resource may not have// completed construction when this callback runs. Therefore, all fields of the// resource referenced by "asyncId" may not have been populated.functioninit(asyncId, type, triggerAsyncId, resource) { }// before() is called just before the resource's callback is called. It can be// called 0-N times for handles (such as TCPWrap), and will be called exactly 1// time for requests (such as FSReqCallback).functionbefore(asyncId) { }// after() is called just after the resource's callback has finished.functionafter(asyncId) { }// destroy() is called when the resource is destroyed.functiondestroy(asyncId) { }// promiseResolve() is called only for promise resources, when the// resolve() function passed to the Promise constructor is invoked// (either directly or through other means of resolving a promise).functionpromiseResolve(asyncId) { }const async_hooks =require('node: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, promiseResolve });// 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();//// The following are the callbacks that can be passed to createHook().//// init() is called during object construction. The resource may not have// completed construction when this callback runs. Therefore, all fields of the// resource referenced by "asyncId" may not have been populated.functioninit(asyncId, type, triggerAsyncId, resource) { }// before() is called just before the resource's callback is called. It can be// called 0-N times for handles (such as TCPWrap), and will be called exactly 1// time for requests (such as FSReqCallback).functionbefore(asyncId) { }// after() is called just after the resource's callback has finished.functionafter(asyncId) { }// destroy() is called when the resource is destroyed.functiondestroy(asyncId) { }// promiseResolve() is called only for promise resources, when the// resolve() function passed to the Promise constructor is invoked// (either directly or through other means of resolving a promise).functionpromiseResolve(asyncId) { }

async_hooks.createHook(callbacks)#

Added in: v8.1.0

Registers functions to be called for different lifetime events of each asyncoperation.

The callbacksinit()/before()/after()/destroy() are called for therespective asynchronous event during a resource's lifetime.

All callbacks are optional. For example, if only resource cleanup needs tobe tracked, then only thedestroy callback needs to be passed. Thespecifics of all functions that can be passed tocallbacks is in theHook Callbacks section.

import { createHook }from'node:async_hooks';const asyncHook =createHook({init(asyncId, type, triggerAsyncId, resource) { },destroy(asyncId) { },});const async_hooks =require('node:async_hooks');const asyncHook = async_hooks.createHook({init(asyncId, type, triggerAsyncId, resource) { },destroy(asyncId) { },});

The callbacks will be inherited via the prototype chain:

classMyAsyncCallbacks {init(asyncId, type, triggerAsyncId, resource) { }destroy(asyncId) {}}classMyAddedCallbacksextendsMyAsyncCallbacks {before(asyncId) { }after(asyncId) { }}const asyncHook = async_hooks.createHook(newMyAddedCallbacks());

Because promises are asynchronous resources whose lifecycle is trackedvia the async hooks mechanism, theinit(),before(),after(), anddestroy() callbacksmust not be async functions that return promises.

Error handling#

If anyAsyncHook callbacks throw, the application will print the stack traceand exit. The exit path does follow that of an uncaught exception, butall'uncaughtException' listeners are removed, thus forcing the process toexit. The'exit' callbacks will still be called unless the application is runwith--abort-on-uncaught-exception, in which case a stack trace will beprinted and the application exits, leaving a core file.

The reason for this error handling behavior is that these callbacks are runningat potentially volatile points in an object's lifetime, for example duringclass construction and destruction. Because of this, it is deemed necessary tobring down the process quickly in order to prevent an unintentional abort in thefuture. This is subject to change in the future if a comprehensive analysis isperformed to ensure an exception can follow the normal control flow withoutunintentional side effects.

Printing inAsyncHook callbacks#

Because printing to the console is an asynchronous operation,console.log()will causeAsyncHook callbacks to be called. Usingconsole.log() orsimilar asynchronous operations inside anAsyncHook callback function willcause an infinite recursion. An easy solution to this when debugging is to use asynchronous logging operation such asfs.writeFileSync(file, msg, flag).This will print to the file and will not invokeAsyncHook recursively becauseit is synchronous.

import { writeFileSync }from'node:fs';import { format }from'node:util';functiondebug(...args) {// Use a function like this one when debugging inside an AsyncHook callbackwriteFileSync('log.out',`${format(...args)}\n`, {flag:'a' });}const fs =require('node:fs');const util =require('node:util');functiondebug(...args) {// Use a function like this one when debugging inside an AsyncHook callback  fs.writeFileSync('log.out',`${util.format(...args)}\n`, {flag:'a' });}

If an asynchronous operation is needed for logging, it is possible to keeptrack of what caused the asynchronous operation using the informationprovided byAsyncHook itself. The logging should then be skipped whenit was the logging itself that caused theAsyncHook callback to be called. Bydoing this, the otherwise infinite recursion is broken.

Class:AsyncHook#

The classAsyncHook exposes an interface for tracking lifetime eventsof asynchronous operations.

asyncHook.enable()#

Enable the callbacks for a givenAsyncHook instance. If no callbacks areprovided, enabling is a no-op.

TheAsyncHook instance is disabled by default. If theAsyncHook instanceshould be enabled immediately after creation, the following pattern can be used.

import { createHook }from'node:async_hooks';const hook =createHook(callbacks).enable();const async_hooks =require('node:async_hooks');const hook = async_hooks.createHook(callbacks).enable();

asyncHook.disable()#

Disable the callbacks for a givenAsyncHook instance from the global pool ofAsyncHook callbacks to be executed. Once a hook has been disabled it will notbe called again until enabled.

For API consistencydisable() also returns theAsyncHook instance.

Hook callbacks#

Key events in the lifetime of asynchronous events have been categorized intofour areas: instantiation, before/after the callback is called, and when theinstance is destroyed.

init(asyncId, type, triggerAsyncId, resource)#
  • asyncId<number> A unique ID for the async resource.
  • type<string> The type of the async resource.
  • triggerAsyncId<number> The unique ID of the async resource in whoseexecution context this async resource was created.
  • resource<Object> Reference to the resource representing the asyncoperation, needs to be released duringdestroy.

Called when a class is constructed that has thepossibility to emit anasynchronous event. Thisdoes not mean the instance must callbefore/after beforedestroy is called, only that the possibilityexists.

This behavior can be observed by doing something like opening a resource thenclosing it before the resource can be used. The following snippet demonstratesthis.

import { createServer }from'node:net';createServer().listen(function() {this.close(); });// ORclearTimeout(setTimeout(() => {},10));require('node:net').createServer().listen(function() {this.close(); });// ORclearTimeout(setTimeout(() => {},10));

Every new resource is assigned an ID that is unique within the scope of thecurrent Node.js instance.

type#

Thetype is a string identifying the type of resource that causedinit to be called. Generally, it will correspond to the name of theresource's constructor.

Thetype of resources created by Node.js itself can change in any Node.jsrelease. Valid values includeTLSWRAP,TCPWRAP,TCPSERVERWRAP,GETADDRINFOREQWRAP,FSREQCALLBACK,Microtask, andTimeout. Inspect the source code of the Node.js version usedto get the full list.

Furthermore users ofAsyncResource create async resources independentof Node.js itself.

There is also thePROMISE resource type, which is used to trackPromiseinstances and asynchronous work scheduled by them.

Users are able to define their owntype when using the public embedder API.

It is possible to have type name collisions. Embedders are encouraged to useunique prefixes, such as the npm package name, to prevent collisions whenlistening to the hooks.

triggerAsyncId#

triggerAsyncId is theasyncId of the resource that caused (or "triggered")the new resource to initialize and that causedinit to call. This is differentfromasync_hooks.executionAsyncId() that only showswhen a resource wascreated, whiletriggerAsyncId showswhy a resource was created.

The following is a simple demonstration oftriggerAsyncId:

import { createHook, executionAsyncId }from'node:async_hooks';import { stdout }from'node:process';import netfrom'node:net';import fsfrom'node:fs';createHook({init(asyncId, type, triggerAsyncId) {const eid =executionAsyncId();    fs.writeSync(      stdout.fd,`${type}(${asyncId}): trigger:${triggerAsyncId} execution:${eid}\n`);  },}).enable();net.createServer((conn) => {}).listen(8080);const { createHook, executionAsyncId } =require('node:async_hooks');const { stdout } =require('node:process');const net =require('node:net');const fs =require('node:fs');createHook({init(asyncId, type, triggerAsyncId) {const eid =executionAsyncId();    fs.writeSync(      stdout.fd,`${type}(${asyncId}): trigger:${triggerAsyncId} execution:${eid}\n`);  },}).enable();net.createServer((conn) => {}).listen(8080);

Output when hitting the server withnc localhost 8080:

TCPSERVERWRAP(5): trigger: 1 execution: 1TCPWRAP(7): trigger: 5 execution: 0

TheTCPSERVERWRAP is the server which receives the connections.

TheTCPWRAP is the new connection from the client. When a newconnection is made, theTCPWrap instance is immediately constructed. Thishappens outside of any JavaScript stack. (AnexecutionAsyncId() of0 meansthat it is being executed from C++ with no JavaScript stack above it.) With onlythat information, it would be impossible to link resources together interms of what caused them to be created, sotriggerAsyncId is given the taskof propagating what resource is responsible for the new resource's existence.

resource#

resource is an object that represents the actual async resource that hasbeen initialized. The API to access the object may be specified by thecreator of the resource. Resources created by Node.js itself are internaland may change at any time. Therefore no API is specified for these.

In some cases the resource object is reused for performance reasons, it isthus not safe to use it as a key in aWeakMap or add properties to it.

Asynchronous context example#

The context tracking use case is covered by the stable APIAsyncLocalStorage.This example only illustrates async hooks operation butAsyncLocalStoragefits better to this use case.

The following is an example with additional information about the calls toinit between thebefore andafter calls, specifically what thecallback tolisten() will look like. The output formatting is slightly moreelaborate to make calling context easier to see.

import async_hooksfrom'node:async_hooks';import fsfrom'node:fs';import netfrom'node:net';import { stdout }from'node:process';const { fd } = stdout;let indent =0;async_hooks.createHook({init(asyncId, type, triggerAsyncId) {const eid = async_hooks.executionAsyncId();const indentStr =' '.repeat(indent);    fs.writeSync(      fd,`${indentStr}${type}(${asyncId}):` +` trigger:${triggerAsyncId} execution:${eid}\n`);  },before(asyncId) {const indentStr =' '.repeat(indent);    fs.writeSync(fd,`${indentStr}before:${asyncId}\n`);    indent +=2;  },after(asyncId) {    indent -=2;const indentStr =' '.repeat(indent);    fs.writeSync(fd,`${indentStr}after:${asyncId}\n`);  },destroy(asyncId) {const indentStr =' '.repeat(indent);    fs.writeSync(fd,`${indentStr}destroy:${asyncId}\n`);  },}).enable();net.createServer(() => {}).listen(8080,() => {// Let's wait 10ms before logging the server started.setTimeout(() => {console.log('>>>', async_hooks.executionAsyncId());  },10);});const async_hooks =require('node:async_hooks');const fs =require('node:fs');const net =require('node:net');const { fd } = process.stdout;let indent =0;async_hooks.createHook({init(asyncId, type, triggerAsyncId) {const eid = async_hooks.executionAsyncId();const indentStr =' '.repeat(indent);    fs.writeSync(      fd,`${indentStr}${type}(${asyncId}):` +` trigger:${triggerAsyncId} execution:${eid}\n`);  },before(asyncId) {const indentStr =' '.repeat(indent);    fs.writeSync(fd,`${indentStr}before:${asyncId}\n`);    indent +=2;  },after(asyncId) {    indent -=2;const indentStr =' '.repeat(indent);    fs.writeSync(fd,`${indentStr}after:${asyncId}\n`);  },destroy(asyncId) {const indentStr =' '.repeat(indent);    fs.writeSync(fd,`${indentStr}destroy:${asyncId}\n`);  },}).enable();net.createServer(() => {}).listen(8080,() => {// Let's wait 10ms before logging the server started.setTimeout(() => {console.log('>>>', async_hooks.executionAsyncId());  },10);});

Output from only starting the server:

TCPSERVERWRAP(5): trigger: 1 execution: 1TickObject(6): trigger: 5 execution: 1before:  6  Timeout(7): trigger: 6 execution: 6after:   6destroy: 6before:  7>>> 7  TickObject(8): trigger: 7 execution: 7after:   7before:  8after:   8

As illustrated in the example,executionAsyncId() andexecution each specifythe value of the current execution context; which is delineated by calls tobefore andafter.

Only usingexecution to graph resource allocation results in the following:

  root(1)     ^     |TickObject(6)     ^     | Timeout(7)

TheTCPSERVERWRAP is not part of this graph, even though it was the reason forconsole.log() being called. This is because binding to a port without a hostname is asynchronous operation, but to maintain a completely asynchronousAPI the user's callback is placed in aprocess.nextTick(). Which is whyTickObject is present in the output and is a 'parent' for.listen()callback.

The graph only showswhen a resource was created, notwhy, so to trackthewhy usetriggerAsyncId. Which can be represented with the followinggraph:

 bootstrap(1)     |     ˅TCPSERVERWRAP(5)     |     ˅ TickObject(6)     |     ˅  Timeout(7)
before(asyncId)#

When an asynchronous operation is initiated (such as a TCP server receiving anew connection) or completes (such as writing data to disk) a callback iscalled to notify the user. Thebefore callback is called just before saidcallback is executed.asyncId is the unique identifier assigned to theresource about to execute the callback.

Thebefore callback will be called 0 to N times. Thebefore callbackwill typically be called 0 times if the asynchronous operation was cancelledor, for example, if no connections are received by a TCP server. Persistentasynchronous resources like a TCP server will typically call thebeforecallback multiple times, while other operations likefs.open() will callit only once.

after(asyncId)#

Called immediately after the callback specified inbefore is completed.

If an uncaught exception occurs during execution of the callback, thenafterwill runafter the'uncaughtException' event is emitted or adomain'shandler runs.

destroy(asyncId)#

Called after the resource corresponding toasyncId is destroyed. It is alsocalled asynchronously from the embedder APIemitDestroy().

Some resources depend on garbage collection for cleanup, so if a reference ismade to theresource object passed toinit it is possible thatdestroywill never be called, causing a memory leak in the application. If the resourcedoes not depend on garbage collection, then this will not be an issue.

Using the destroy hook results in additional overhead because it enablestracking ofPromise instances via the garbage collector.

promiseResolve(asyncId)#
Added in: v8.6.0

Called when theresolve function passed to thePromise constructor isinvoked (either directly or through other means of resolving a promise).

resolve() does not do any observable synchronous work.

ThePromise is not necessarily fulfilled or rejected at this point if thePromise was resolved by assuming the state of anotherPromise.

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

calls the following callbacks:

init for PROMISE with id 5, trigger id: 1  promise resolve 5      # corresponds to resolve(true)init for PROMISE with id 6, trigger id: 5  # the Promise returned by then()  before 6               # the then() callback is entered  promise resolve 6      # the then() callback resolves the promise by returning  after 6

async_hooks.executionAsyncResource()#

Added in: v13.9.0, v12.17.0
  • Returns:<Object> The resource representing the current execution.Useful to store data within the resource.

Resource objects returned byexecutionAsyncResource() are most often internalNode.js handle objects with undocumented APIs. Using any functions or propertieson the object is likely to crash your application and should be avoided.

UsingexecutionAsyncResource() in the top-level execution context willreturn an empty object as there is no handle or request object to use,but having an object representing the top-level can be helpful.

import { open }from'node:fs';import { executionAsyncId, executionAsyncResource }from'node:async_hooks';console.log(executionAsyncId(),executionAsyncResource());// 1 {}open(newURL(import.meta.url),'r',(err, fd) => {console.log(executionAsyncId(),executionAsyncResource());// 7 FSReqWrap});const { open } =require('node:fs');const { executionAsyncId, executionAsyncResource } =require('node:async_hooks');console.log(executionAsyncId(),executionAsyncResource());// 1 {}open(__filename,'r',(err, fd) => {console.log(executionAsyncId(),executionAsyncResource());// 7 FSReqWrap});

This can be used to implement continuation local storage without theuse of a trackingMap to store the metadata:

import { createServer }from'node:http';import {  executionAsyncId,  executionAsyncResource,  createHook,}from'node:async_hooks';const sym =Symbol('state');// Private symbol to avoid pollutioncreateHook({init(asyncId, type, triggerAsyncId, resource) {const cr =executionAsyncResource();if (cr) {      resource[sym] = cr[sym];    }  },}).enable();const server =createServer((req, res) => {executionAsyncResource()[sym] = {state: req.url };setTimeout(function() {    res.end(JSON.stringify(executionAsyncResource()[sym]));  },100);}).listen(3000);const { createServer } =require('node:http');const {  executionAsyncId,  executionAsyncResource,  createHook,} =require('node:async_hooks');const sym =Symbol('state');// Private symbol to avoid pollutioncreateHook({init(asyncId, type, triggerAsyncId, resource) {const cr =executionAsyncResource();if (cr) {      resource[sym] = cr[sym];    }  },}).enable();const server =createServer((req, res) => {executionAsyncResource()[sym] = {state: req.url };setTimeout(function() {    res.end(JSON.stringify(executionAsyncResource()[sym]));  },100);}).listen(3000);

async_hooks.executionAsyncId()#

History
VersionChanges
v8.2.0

Renamed fromcurrentId.

v8.1.0

Added in: v8.1.0

  • Returns:<number> TheasyncId of the current execution context. Useful totrack when something calls.
import { executionAsyncId }from'node:async_hooks';import fsfrom'node:fs';console.log(executionAsyncId());// 1 - bootstrapconst path ='.';fs.open(path,'r',(err, fd) => {console.log(executionAsyncId());// 6 - open()});const async_hooks =require('node:async_hooks');const fs =require('node:fs');console.log(async_hooks.executionAsyncId());// 1 - bootstrapconst path ='.';fs.open(path,'r',(err, fd) => {console.log(async_hooks.executionAsyncId());// 6 - open()});

The ID returned fromexecutionAsyncId() is related to execution timing, notcausality (which is covered bytriggerAsyncId()):

const server = net.createServer((conn) => {// Returns the ID of the server, not of the new connection, because the// callback runs in the execution scope of the server's MakeCallback().  async_hooks.executionAsyncId();}).listen(port,() => {// Returns the ID of a TickObject (process.nextTick()) because all// callbacks passed to .listen() are wrapped in a nextTick().  async_hooks.executionAsyncId();});

Promise contexts may not get preciseexecutionAsyncIds by default.See the section onpromise execution tracking.

async_hooks.triggerAsyncId()#

  • Returns:<number> The ID of the resource responsible for calling the callbackthat is currently being executed.
const server = net.createServer((conn) => {// The resource that caused (or triggered) this callback to be called// was that of the new connection. Thus the return value of triggerAsyncId()// is the asyncId of "conn".  async_hooks.triggerAsyncId();}).listen(port,() => {// Even though all callbacks passed to .listen() are wrapped in a nextTick()// the callback itself exists because the call to the server's .listen()// was made. So the return value would be the ID of the server.  async_hooks.triggerAsyncId();});

Promise contexts may not get validtriggerAsyncIds by default. Seethe section onpromise execution tracking.

async_hooks.asyncWrapProviders#

Added in: v17.2.0, v16.14.0
  • Returns: A map of provider types to the corresponding numeric id.This map contains all the event types that might be emitted by theasync_hooks.init() event.

This feature suppresses the deprecated usage ofprocess.binding('async_wrap').Providers.See:DEP0111

Promise execution tracking#

By default, promise executions are not assignedasyncIds due to the relativelyexpensive nature of thepromise introspection API provided byV8. This means that programs using promises orasync/await will not getcorrect execution and trigger ids for promise callback contexts by default.

import { executionAsyncId, triggerAsyncId }from'node:async_hooks';Promise.resolve(1729).then(() => {console.log(`eid${executionAsyncId()} tid${triggerAsyncId()}`);});// produces:// eid 1 tid 0const { executionAsyncId, triggerAsyncId } =require('node:async_hooks');Promise.resolve(1729).then(() => {console.log(`eid${executionAsyncId()} tid${triggerAsyncId()}`);});// produces:// eid 1 tid 0

Observe that thethen() callback claims to have executed in the context of theouter scope even though there was an asynchronous hop involved. Also,thetriggerAsyncId value is0, which means that we are missing context aboutthe resource that caused (triggered) thethen() callback to be executed.

Installing async hooks viaasync_hooks.createHook enables promise executiontracking:

import { createHook, executionAsyncId, triggerAsyncId }from'node:async_hooks';createHook({init() {} }).enable();// forces PromiseHooks to be enabled.Promise.resolve(1729).then(() => {console.log(`eid${executionAsyncId()} tid${triggerAsyncId()}`);});// produces:// eid 7 tid 6const { createHook, executionAsyncId, triggerAsyncId } =require('node:async_hooks');createHook({init() {} }).enable();// forces PromiseHooks to be enabled.Promise.resolve(1729).then(() => {console.log(`eid${executionAsyncId()} tid${triggerAsyncId()}`);});// produces:// eid 7 tid 6

In this example, adding any actual hook function enabled the tracking ofpromises. There are two promises in the example above; the promise created byPromise.resolve() and the promise returned by the call tothen(). In theexample above, the first promise got theasyncId6 and the latter gotasyncId7. During the execution of thethen() callback, we are executingin the context of promise withasyncId7. This promise was triggered byasync resource6.

Another subtlety with promises is thatbefore andafter callbacks are runonly on chained promises. That means promises not created bythen()/catch()will not have thebefore andafter callbacks fired on them. For more detailssee the details of the V8PromiseHooks API.

JavaScript embedder API#

Library developers that handle their own asynchronous resources performing taskslike I/O, connection pooling, or managing callback queues may use theAsyncResource JavaScript API so that all the appropriate callbacks are called.

Class:AsyncResource#

The documentation for this class has movedAsyncResource.

Class:AsyncLocalStorage#

The documentation for this class has movedAsyncLocalStorage.