Async context tracking APIs are stable and recommended for production use. They provide a performant and memory-safe way to track context across asynchronous operations.
Overview
The async context tracking classes allow you to store data throughout the lifetime of a web request or any other asynchronous duration, similar to thread-local storage in other languages.
import { AsyncLocalStorage, AsyncResource } from 'node:async_hooks';
// or
const { AsyncLocalStorage, AsyncResource } = require('node:async_hooks');
AsyncLocalStorage
The AsyncLocalStorage class creates stores that stay coherent through asynchronous operations.
Constructor
new AsyncLocalStorage([options])
Creates a new instance of AsyncLocalStorage.
Optional configurationThe default value when no store is provided
A name for the AsyncLocalStorage instance
import { AsyncLocalStorage } from 'node:async_hooks';
const asyncLocalStorage = new AsyncLocalStorage({
defaultValue: { userId: 'anonymous' },
name: 'requestContext'
});
Instance Methods
run(store, callback, …args)
Runs a function synchronously within a context and returns its value.
The store value to use for this context
The function to run within the context
Optional arguments to pass to the callback
The return value of the callback function
import http from 'node:http';
import { AsyncLocalStorage } from 'node:async_hooks';
const asyncLocalStorage = new AsyncLocalStorage();
function logWithId(msg) {
const id = asyncLocalStorage.getStore();
console.log(`${id !== undefined ? id : '-'}:`, msg);
}
let idSeq = 0;
http.createServer((req, res) => {
asyncLocalStorage.run(idSeq++, () => {
logWithId('start');
setImmediate(() => {
logWithId('finish');
res.end();
});
});
}).listen(8080);
getStore()
Returns the current store value.
The current store, or undefined if called outside of run() or enterWith()
const store = asyncLocalStorage.getStore();
if (store) {
console.log('User ID:', store.userId);
}
enterWith(store)
Experimental - Transitions into the context for the remainder of the current synchronous execution.
const store = { id: 1 };
asyncLocalStorage.enterWith(store);
asyncLocalStorage.getStore(); // Returns { id: 1 }
someAsyncOperation(() => {
asyncLocalStorage.getStore(); // Still returns { id: 1 }
});
exit(callback, …args)
Experimental - Runs a function outside of the current context.
Function to run outside the context
asyncLocalStorage.run(new Map(), () => {
asyncLocalStorage.getStore(); // Returns the Map
asyncLocalStorage.exit(() => {
asyncLocalStorage.getStore(); // Returns undefined
});
asyncLocalStorage.getStore(); // Returns the Map again
});
disable()
Experimental - Disables the instance. All subsequent calls to getStore() return undefined.
Static Methods
AsyncLocalStorage.bind(fn)
Binds the given function to the current execution context.
A new function that calls fn within the captured context
AsyncLocalStorage.snapshot()
Captures the current execution context and returns a function wrapper.
A function that accepts a function and runs it in the captured context
const asyncLocalStorage = new AsyncLocalStorage();
const runInAsyncScope = asyncLocalStorage.run(123, () =>
AsyncLocalStorage.snapshot()
);
const result = asyncLocalStorage.run(321, () =>
runInAsyncScope(() => asyncLocalStorage.getStore())
);
console.log(result); // Returns 123
Properties
name
The name of the AsyncLocalStorage instance if provided
Complete Example: HTTP Request Context
import http from 'node:http';
import { AsyncLocalStorage } from 'node:async_hooks';
const asyncLocalStorage = new AsyncLocalStorage();
function logWithId(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 here
setImmediate(() => {
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
AsyncResource
The AsyncResource class is designed to be extended by embedders’ async resources to trigger lifetime events.
Constructor
new AsyncResource(type[, options])
Creates a new AsyncResource.
Configuration optionsThe ID of the execution context that created this async event. Default: executionAsyncId()
Disables automatic emitDestroy when garbage collected
import { AsyncResource, executionAsyncId } from 'node:async_hooks';
const asyncResource = new AsyncResource(
'MyResource',
{ triggerAsyncId: executionAsyncId() }
);
Instance Methods
runInAsyncScope(fn, thisArg, …args)
Runs a function in the execution context of the async resource.
The receiver for the function call
Optional arguments to pass to the function
class DBQuery extends AsyncResource {
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();
}
}
emitDestroy()
Call all destroy hooks. Should only be called once.
Reference to asyncResource
asyncId()
Returns the unique ID assigned to the resource.
triggerAsyncId()
Returns the trigger ID passed to the constructor.
The same triggerAsyncId passed to the constructor
Static Methods
AsyncResource.bind(fn[, type[, thisArg]])
Binds the given function to the current execution context.
Optional name for the underlying AsyncResource
The receiver for the function
bind(fn[, thisArg])
Instance method to bind a function to this AsyncResource’s scope.
Worker Thread Pool Example
Using AsyncResource to properly track async operations in a worker pool:
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');
class WorkerPoolTaskInfo extends AsyncResource {
constructor(callback) {
super('WorkerPoolTaskInfo');
this.callback = callback;
}
done(err, result) {
this.runInAsyncScope(this.callback, null, err, result);
this.emitDestroy();
}
}
export default class WorkerPool extends EventEmitter {
constructor(numThreads) {
super();
this.numThreads = numThreads;
this.workers = [];
this.freeWorkers = [];
this.tasks = [];
for (let i = 0; i < numThreads; i++)
this.addNewWorker();
this.on(kWorkerFreedEvent, () => {
if (this.tasks.length > 0) {
const { task, callback } = this.tasks.shift();
this.runTask(task, callback);
}
});
}
addNewWorker() {
const worker = new Worker(new URL('task_processor.js', import.meta.url));
worker.on('message', (result) => {
worker[kTaskInfo].done(null, result);
worker[kTaskInfo] = null;
this.freeWorkers.push(worker);
this.emit(kWorkerFreedEvent);
});
worker.on('error', (err) => {
if (worker[kTaskInfo])
worker[kTaskInfo].done(err, null);
else
this.emit('error', err);
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) {
this.tasks.push({ task, callback });
return;
}
const worker = this.freeWorkers.pop();
worker[kTaskInfo] = new WorkerPoolTaskInfo(callback);
worker.postMessage(task);
}
close() {
for (const worker of this.workers) worker.terminate();
}
}
Usage with async/await
When using async functions, use run() to ensure the store is properly scoped:
import { AsyncLocalStorage } from 'node:async_hooks';
const asyncLocalStorage = new AsyncLocalStorage();
async function fn() {
await asyncLocalStorage.run(new Map(), () => {
asyncLocalStorage.getStore().set('key', value);
return foo(); // The return value of foo will be awaited
});
}
Troubleshooting: Context Loss
If context is lost in callback-based APIs:
- Promisify callback APIs using
util.promisify()
- Use AsyncResource for custom thenable implementations
- Debug by logging
asyncLocalStorage.getStore() after suspicious calls
import { promisify } from 'node:util';
import { AsyncResource } from 'node:async_hooks';
// Option 1: Promisify
const readFileAsync = promisify(fs.readFile);
// Option 2: Use AsyncResource
class CustomAsync extends AsyncResource {
constructor() {
super('CustomAsync');
}
doWork(callback) {
someAsyncOp((err, result) => {
this.runInAsyncScope(callback, null, err, result);
});
}
}
Integration with EventEmitter
Bind event listeners to the correct execution context:
import { createServer } from 'node:http';
import { AsyncResource } from 'node:async_hooks';
const server = createServer((req, res) => {
req.on('close', AsyncResource.bind(() => {
// Execution context is bound to the outer scope
}));
req.on('close', () => {
// Execution context is bound to where 'close' emits
});
res.end();
}).listen(3000);
Best Practices
Performance Tips
- Prefer
run() over enterWith() for better scoping control
- Use
AsyncLocalStorage instead of manually tracking with AsyncResource for simple use cases
- Remember that each AsyncLocalStorage instance maintains independent storage
Memory Management
- Call
disable() when AsyncLocalStorage is no longer needed
- Remember to call
emitDestroy() on AsyncResource instances
- Stores are garbage collected with their corresponding async resources