Asynchronous context tracking#

Stability: 2 - Stable

Source Code:lib/async_hooks.js

Introduction#

These classes are used to associate state and propagate it throughoutcallbacks and promise chains.They allow storing data throughout the lifetime of a web requestor any other asynchronous duration. It is similar to thread-local storagein other languages.

TheAsyncLocalStorage andAsyncResource classes are part of thenode:async_hooks module:

import {AsyncLocalStorage,AsyncResource }from'node:async_hooks';const {AsyncLocalStorage,AsyncResource } =require('node:async_hooks');

Class:AsyncLocalStorage#

History
VersionChanges
v16.4.0

AsyncLocalStorage is now Stable. Previously, it had been Experimental.

v13.10.0, v12.17.0

Added in: v13.10.0, v12.17.0

This class creates stores that stay coherent through asynchronous operations.

While you can create your own implementation on top of thenode:async_hooksmodule,AsyncLocalStorage should be preferred as it is a performant and memorysafe implementation that involves significant optimizations that are non-obviousto implement.

The following example usesAsyncLocalStorage to build a simple loggerthat assigns IDs to incoming HTTP requests and includes them in messageslogged within each request.

import httpfrom'node:http';import {AsyncLocalStorage }from'node:async_hooks';const asyncLocalStorage =newAsyncLocalStorage();functionlogWithId(msg) {const id = asyncLocalStorage.getStore();console.log(`${id !==undefined ? id :'-'}:`, msg);}let idSeq =0;http.createServer((req, res) => {  asyncLocalStorage.run(idSeq++,() => {logWithId('start');// Imagine any chain of async operations heresetImmediate(() => {logWithId('finish');      res.end();    });  });}).listen(8080);http.get('http://localhost:8080');http.get('http://localhost:8080');// Prints://   0: start//   0: finish//   1: start//   1: finishconst http =require('node:http');const {AsyncLocalStorage } =require('node:async_hooks');const asyncLocalStorage =newAsyncLocalStorage();functionlogWithId(msg) {const id = asyncLocalStorage.getStore();console.log(`${id !==undefined ? id :'-'}:`, msg);}let idSeq =0;http.createServer((req, res) => {  asyncLocalStorage.run(idSeq++,() => {logWithId('start');// Imagine any chain of async operations heresetImmediate(() => {logWithId('finish');      res.end();    });  });}).listen(8080);http.get('http://localhost:8080');http.get('http://localhost:8080');// Prints://   0: start//   0: finish//   1: start//   1: finish

Each instance ofAsyncLocalStorage maintains an independent storage context.Multiple instances can safely exist simultaneously without risk of interferingwith each other's data.

new AsyncLocalStorage([options])#

History
VersionChanges
v24.0.0

AdddefaultValue andname options.

v19.7.0, v18.16.0

Removed experimental onPropagate option.

v19.2.0, v18.13.0

Add option onPropagate.

v13.10.0, v12.17.0

Added in: v13.10.0, v12.17.0

  • options<Object>
    • defaultValue<any> The default value to be used when no store is provided.
    • name<string> A name for theAsyncLocalStorage value.

Creates a new instance ofAsyncLocalStorage. Store is only provided within arun() call or after anenterWith() call.

Static method:AsyncLocalStorage.bind(fn)#

History
VersionChanges
v23.11.0, v22.15.0

Marking the API stable.

v19.8.0, v18.16.0

Added in: v19.8.0, v18.16.0

  • fn<Function> The function to bind to the current execution context.
  • Returns:<Function> A new function that callsfn within the capturedexecution context.

Binds the given function to the current execution context.

Static method:AsyncLocalStorage.snapshot()#

History
VersionChanges
v23.11.0, v22.15.0

Marking the API stable.

v19.8.0, v18.16.0

Added in: v19.8.0, v18.16.0

  • Returns:<Function> A new function with the signature(fn: (...args) : R, ...args) : R.

Captures the current execution context and returns a function that accepts afunction as an argument. Whenever the returned function is called, itcalls the function passed to it within the captured context.

const asyncLocalStorage =newAsyncLocalStorage();const runInAsyncScope = asyncLocalStorage.run(123,() =>AsyncLocalStorage.snapshot());const result = asyncLocalStorage.run(321,() =>runInAsyncScope(() => asyncLocalStorage.getStore()));console.log(result);// returns 123

AsyncLocalStorage.snapshot() can replace the use of AsyncResource for simpleasync context tracking purposes, for example:

classFoo {  #runInAsyncScope =AsyncLocalStorage.snapshot();get() {returnthis.#runInAsyncScope(() => asyncLocalStorage.getStore()); }}const foo = asyncLocalStorage.run(123,() =>newFoo());console.log(asyncLocalStorage.run(321,() => foo.get()));// returns 123

asyncLocalStorage.disable()#

Added in: v13.10.0, v12.17.0

Stability: 1 - Experimental

Disables the instance ofAsyncLocalStorage. All subsequent callstoasyncLocalStorage.getStore() will returnundefined untilasyncLocalStorage.run() orasyncLocalStorage.enterWith() is called again.

When callingasyncLocalStorage.disable(), all current contexts linked to theinstance will be exited.

CallingasyncLocalStorage.disable() is required before theasyncLocalStorage can be garbage collected. This does not apply to storesprovided by theasyncLocalStorage, as those objects are garbage collectedalong with the corresponding async resources.

Use this method when theasyncLocalStorage is not in use anymorein the current process.

asyncLocalStorage.getStore()#

Added in: v13.10.0, v12.17.0

Returns the current store.If called outside of an asynchronous context initialized bycallingasyncLocalStorage.run() orasyncLocalStorage.enterWith(), itreturnsundefined.

asyncLocalStorage.enterWith(store)#

Added in: v13.11.0, v12.17.0

Stability: 1 - Experimental

Transitions into the context for the remainder of the currentsynchronous execution and then persists the store through any followingasynchronous calls.

Example:

const store = {id:1 };// Replaces previous store with the given store objectasyncLocalStorage.enterWith(store);asyncLocalStorage.getStore();// Returns the store objectsomeAsyncOperation(() => {  asyncLocalStorage.getStore();// Returns the same object});

This transition will continue for theentire synchronous execution.This means that if, for example, the context is entered within an eventhandler subsequent event handlers will also run within that context unlessspecifically bound to another context with anAsyncResource. That is whyrun() should be preferred overenterWith() unless there are strong reasonsto use the latter method.

const store = {id:1 };emitter.on('my-event',() => {  asyncLocalStorage.enterWith(store);});emitter.on('my-event',() => {  asyncLocalStorage.getStore();// Returns the same object});asyncLocalStorage.getStore();// Returns undefinedemitter.emit('my-event');asyncLocalStorage.getStore();// Returns the same object

asyncLocalStorage.name#

Added in: v24.0.0

The name of theAsyncLocalStorage instance if provided.

asyncLocalStorage.run(store, callback[, ...args])#

Added in: v13.10.0, v12.17.0

Runs a function synchronously within a context and returns itsreturn value. The store is not accessible outside of the callback function.The store is accessible to any asynchronous operations created within thecallback.

The optionalargs are passed to the callback function.

If the callback function throws an error, the error is thrown byrun() too.The stacktrace is not impacted by this call and the context is exited.

Example:

const store = {id:2 };try {  asyncLocalStorage.run(store,() => {    asyncLocalStorage.getStore();// Returns the store objectsetTimeout(() => {      asyncLocalStorage.getStore();// Returns the store object    },200);thrownewError();  });}catch (e) {  asyncLocalStorage.getStore();// Returns undefined// The error will be caught here}

asyncLocalStorage.exit(callback[, ...args])#

Added in: v13.10.0, v12.17.0

Stability: 1 - Experimental

Runs a function synchronously outside of a context and returns itsreturn value. The store is not accessible within the callback function orthe asynchronous operations created within the callback. AnygetStore()call done within the callback function will always returnundefined.

The optionalargs are passed to the callback function.

If the callback function throws an error, the error is thrown byexit() too.The stacktrace is not impacted by this call and the context is re-entered.

Example:

// Within a call to runtry {  asyncLocalStorage.getStore();// Returns the store object or value  asyncLocalStorage.exit(() => {    asyncLocalStorage.getStore();// Returns undefinedthrownewError();  });}catch (e) {  asyncLocalStorage.getStore();// Returns the same object or value// The error will be caught here}

Usage withasync/await#

If, within an async function, only oneawait call is to run within a context,the following pattern should be used:

asyncfunctionfn() {await asyncLocalStorage.run(newMap(),() => {    asyncLocalStorage.getStore().set('key', value);returnfoo();// The return value of foo will be awaited  });}

In this example, the store is only available in the callback function and thefunctions called byfoo. Outside ofrun, callinggetStore will returnundefined.

Troubleshooting: Context loss#

In most cases,AsyncLocalStorage works without issues. In rare situations, thecurrent store is lost in one of the asynchronous operations.

If your code is callback-based, it is enough to promisify it withutil.promisify() so it starts working with native promises.

If you need to use a callback-based API or your code assumesa custom thenable implementation, use theAsyncResource classto associate the asynchronous operation with the correct execution context.Find the function call responsible for the context loss by logging the contentofasyncLocalStorage.getStore() after the calls you suspect are responsiblefor the loss. When the code logsundefined, the last callback called isprobably responsible for the context loss.

Class:AsyncResource#

History
VersionChanges
v16.4.0

AsyncResource is now Stable. Previously, it had been Experimental.

The classAsyncResource is designed to be extended by the embedder's asyncresources. Using this, users can easily trigger the lifetime events of theirown resources.

Theinit hook will trigger when anAsyncResource is instantiated.

The following is an overview of theAsyncResource API.

import {AsyncResource, executionAsyncId }from'node:async_hooks';// AsyncResource() is meant to be extended. Instantiating a// new AsyncResource() also triggers init. If triggerAsyncId is omitted then// async_hook.executionAsyncId() is used.const asyncResource =newAsyncResource(  type, {triggerAsyncId:executionAsyncId(),requireManualDestroy:false },);// Run a function in the execution context of the resource. This will// * establish the context of the resource// * trigger the AsyncHooks before callbacks// * call the provided function `fn` with the supplied arguments// * trigger the AsyncHooks after callbacks// * restore the original execution contextasyncResource.runInAsyncScope(fn, thisArg, ...args);// Call AsyncHooks destroy callbacks.asyncResource.emitDestroy();// Return the unique ID assigned to the AsyncResource instance.asyncResource.asyncId();// Return the trigger ID for the AsyncResource instance.asyncResource.triggerAsyncId();const {AsyncResource, executionAsyncId } =require('node:async_hooks');// AsyncResource() is meant to be extended. Instantiating a// new AsyncResource() also triggers init. If triggerAsyncId is omitted then// async_hook.executionAsyncId() is used.const asyncResource =newAsyncResource(  type, {triggerAsyncId:executionAsyncId(),requireManualDestroy:false },);// Run a function in the execution context of the resource. This will// * establish the context of the resource// * trigger the AsyncHooks before callbacks// * call the provided function `fn` with the supplied arguments// * trigger the AsyncHooks after callbacks// * restore the original execution contextasyncResource.runInAsyncScope(fn, thisArg, ...args);// Call AsyncHooks destroy callbacks.asyncResource.emitDestroy();// Return the unique ID assigned to the AsyncResource instance.asyncResource.asyncId();// Return the trigger ID for the AsyncResource instance.asyncResource.triggerAsyncId();

new AsyncResource(type[, options])#

  • type<string> The type of async event.
  • options<Object>
    • triggerAsyncId<number> The ID of the execution context that created thisasync event.Default:executionAsyncId().
    • requireManualDestroy<boolean> If set totrue, disablesemitDestroywhen the object is garbage collected. This usually does not need to be set(even ifemitDestroy is called manually), unless the resource'sasyncIdis retrieved and the sensitive API'semitDestroy is called with it.When set tofalse, theemitDestroy call on garbage collectionwill only take place if there is at least one activedestroy hook.Default:false.

Example usage:

classDBQueryextendsAsyncResource {constructor(db) {super('DBQuery');this.db = db;  }getInfo(query, callback) {this.db.get(query,(err, data) => {this.runInAsyncScope(callback,null, err, data);    });  }close() {this.db =null;this.emitDestroy();  }}

Static method:AsyncResource.bind(fn[, type[, thisArg]])#

History
VersionChanges
v20.0.0

TheasyncResource property added to the bound function has been deprecated and will be removed in a future version.

v17.8.0, v16.15.0

Changed the default whenthisArg is undefined to usethis from the caller.

v16.0.0

Added optional thisArg.

v14.8.0, v12.19.0

Added in: v14.8.0, v12.19.0

  • fn<Function> The function to bind to the current execution context.
  • type<string> An optional name to associate with the underlyingAsyncResource.
  • thisArg<any>

Binds the given function to the current execution context.

asyncResource.bind(fn[, thisArg])#

History
VersionChanges
v20.0.0

TheasyncResource property added to the bound function has been deprecated and will be removed in a future version.

v17.8.0, v16.15.0

Changed the default whenthisArg is undefined to usethis from the caller.

v16.0.0

Added optional thisArg.

v14.8.0, v12.19.0

Added in: v14.8.0, v12.19.0

Binds the given function to execute to thisAsyncResource's scope.

asyncResource.runInAsyncScope(fn[, thisArg, ...args])#

Added in: v9.6.0
  • fn<Function> The function to call in the execution context of this asyncresource.
  • thisArg<any> The receiver to be used for the function call.
  • ...args<any> Optional arguments to pass to the function.

Call the provided function with the provided arguments in the execution contextof the async resource. This will establish the context, trigger the AsyncHooksbefore callbacks, call the function, trigger the AsyncHooks after callbacks, andthen restore the original execution context.

asyncResource.emitDestroy()#

Call alldestroy hooks. This should only ever be called once. An error willbe thrown if it is called more than once. Thismust be manually called. Ifthe resource is left to be collected by the GC then thedestroy hooks willnever be called.

asyncResource.asyncId()#

  • Returns:<number> The uniqueasyncId assigned to the resource.

asyncResource.triggerAsyncId()#

  • Returns:<number> The sametriggerAsyncId that is passed to theAsyncResource constructor.

UsingAsyncResource for aWorker thread pool#

The following example shows how to use theAsyncResource class to properlyprovide async tracking for aWorker pool. Other resource pools, such asdatabase connection pools, can follow a similar model.

Assuming that the task is adding two numbers, using a file namedtask_processor.js with the following content:

import { parentPort }from'node:worker_threads';parentPort.on('message',(task) => {  parentPort.postMessage(task.a + task.b);});const { parentPort } =require('node:worker_threads');parentPort.on('message',(task) => {  parentPort.postMessage(task.a + task.b);});

a Worker pool around it could use the following structure:

import {AsyncResource }from'node:async_hooks';import {EventEmitter }from'node:events';import {Worker }from'node:worker_threads';const kTaskInfo =Symbol('kTaskInfo');const kWorkerFreedEvent =Symbol('kWorkerFreedEvent');classWorkerPoolTaskInfoextendsAsyncResource {constructor(callback) {super('WorkerPoolTaskInfo');this.callback = callback;  }done(err, result) {this.runInAsyncScope(this.callback,null, err, result);this.emitDestroy();// `TaskInfo`s are used only once.  }}exportdefaultclassWorkerPoolextendsEventEmitter {constructor(numThreads) {super();this.numThreads = numThreads;this.workers = [];this.freeWorkers = [];this.tasks = [];for (let i =0; i < numThreads; i++)this.addNewWorker();// Any time the kWorkerFreedEvent is emitted, dispatch// the next task pending in the queue, if any.this.on(kWorkerFreedEvent,() => {if (this.tasks.length >0) {const { task, callback } =this.tasks.shift();this.runTask(task, callback);      }    });  }addNewWorker() {const worker =newWorker(newURL('task_processor.js',import.meta.url));    worker.on('message',(result) => {// In case of success: Call the callback that was passed to `runTask`,// remove the `TaskInfo` associated with the Worker, and mark it as free// again.      worker[kTaskInfo].done(null, result);      worker[kTaskInfo] =null;this.freeWorkers.push(worker);this.emit(kWorkerFreedEvent);    });    worker.on('error',(err) => {// In case of an uncaught exception: Call the callback that was passed to// `runTask` with the error.if (worker[kTaskInfo])        worker[kTaskInfo].done(err,null);elsethis.emit('error', err);// Remove the worker from the list and start a new Worker to replace the// current one.this.workers.splice(this.workers.indexOf(worker),1);this.addNewWorker();    });this.workers.push(worker);this.freeWorkers.push(worker);this.emit(kWorkerFreedEvent);  }runTask(task, callback) {if (this.freeWorkers.length ===0) {// No free threads, wait until a worker thread becomes free.this.tasks.push({ task, callback });return;    }const worker =this.freeWorkers.pop();    worker[kTaskInfo] =newWorkerPoolTaskInfo(callback);    worker.postMessage(task);  }close() {for (const workerofthis.workers) worker.terminate();  }}const {AsyncResource } =require('node:async_hooks');const {EventEmitter } =require('node:events');const path =require('node:path');const {Worker } =require('node:worker_threads');const kTaskInfo =Symbol('kTaskInfo');const kWorkerFreedEvent =Symbol('kWorkerFreedEvent');classWorkerPoolTaskInfoextendsAsyncResource {constructor(callback) {super('WorkerPoolTaskInfo');this.callback = callback;  }done(err, result) {this.runInAsyncScope(this.callback,null, err, result);this.emitDestroy();// `TaskInfo`s are used only once.  }}classWorkerPoolextendsEventEmitter {constructor(numThreads) {super();this.numThreads = numThreads;this.workers = [];this.freeWorkers = [];this.tasks = [];for (let i =0; i < numThreads; i++)this.addNewWorker();// Any time the kWorkerFreedEvent is emitted, dispatch// the next task pending in the queue, if any.this.on(kWorkerFreedEvent,() => {if (this.tasks.length >0) {const { task, callback } =this.tasks.shift();this.runTask(task, callback);      }    });  }addNewWorker() {const worker =newWorker(path.resolve(__dirname,'task_processor.js'));    worker.on('message',(result) => {// In case of success: Call the callback that was passed to `runTask`,// remove the `TaskInfo` associated with the Worker, and mark it as free// again.      worker[kTaskInfo].done(null, result);      worker[kTaskInfo] =null;this.freeWorkers.push(worker);this.emit(kWorkerFreedEvent);    });    worker.on('error',(err) => {// In case of an uncaught exception: Call the callback that was passed to// `runTask` with the error.if (worker[kTaskInfo])        worker[kTaskInfo].done(err,null);elsethis.emit('error', err);// Remove the worker from the list and start a new Worker to replace the// current one.this.workers.splice(this.workers.indexOf(worker),1);this.addNewWorker();    });this.workers.push(worker);this.freeWorkers.push(worker);this.emit(kWorkerFreedEvent);  }runTask(task, callback) {if (this.freeWorkers.length ===0) {// No free threads, wait until a worker thread becomes free.this.tasks.push({ task, callback });return;    }const worker =this.freeWorkers.pop();    worker[kTaskInfo] =newWorkerPoolTaskInfo(callback);    worker.postMessage(task);  }close() {for (const workerofthis.workers) worker.terminate();  }}module.exports =WorkerPool;

Without the explicit tracking added by theWorkerPoolTaskInfo objects,it would appear that the callbacks are associated with the individualWorkerobjects. However, the creation of theWorkers is not associated with thecreation of the tasks and does not provide information about when taskswere scheduled.

This pool could be used as follows:

importWorkerPoolfrom'./worker_pool.js';import osfrom'node:os';const pool =newWorkerPool(os.availableParallelism());let finished =0;for (let i =0; i <10; i++) {  pool.runTask({a:42,b:100 },(err, result) => {console.log(i, err, result);if (++finished ===10)      pool.close();  });}constWorkerPool =require('./worker_pool.js');const os =require('node:os');const pool =newWorkerPool(os.availableParallelism());let finished =0;for (let i =0; i <10; i++) {  pool.runTask({a:42,b:100 },(err, result) => {console.log(i, err, result);if (++finished ===10)      pool.close();  });}

IntegratingAsyncResource withEventEmitter#

Event listeners triggered by anEventEmitter may be run in a differentexecution context than the one that was active wheneventEmitter.on() wascalled.

The following example shows how to use theAsyncResource class to properlyassociate an event listener with the correct execution context. The sameapproach can be applied to aStream or a similar event-driven class.

import { createServer }from'node:http';import {AsyncResource, executionAsyncId }from'node:async_hooks';const server =createServer((req, res) => {  req.on('close',AsyncResource.bind(() => {// Execution context is bound to the current outer scope.  }));  req.on('close',() => {// Execution context is bound to the scope that caused 'close' to emit.  });  res.end();}).listen(3000);const { createServer } =require('node:http');const {AsyncResource, executionAsyncId } =require('node:async_hooks');const server =createServer((req, res) => {  req.on('close',AsyncResource.bind(() => {// Execution context is bound to the current outer scope.  }));  req.on('close',() => {// Execution context is bound to the scope that caused 'close' to emit.  });  res.end();}).listen(3000);