The async_hooks module provides an API to track asynchronous resources in Node.js. Here's how to use it for debugging, tracing, and context propagation.
Basic Setup#
1import async_hooks from 'node:async_hooks';
2
3// Create hook instance
4const hook = async_hooks.createHook({
5 init(asyncId, type, triggerAsyncId, resource) {
6 console.log(`Init: ${type} (${asyncId}) triggered by ${triggerAsyncId}`);
7 },
8 before(asyncId) {
9 console.log(`Before: ${asyncId}`);
10 },
11 after(asyncId) {
12 console.log(`After: ${asyncId}`);
13 },
14 destroy(asyncId) {
15 console.log(`Destroy: ${asyncId}`);
16 },
17});
18
19// Enable the hook
20hook.enable();
21
22// Your async code here
23setTimeout(() => {
24 console.log('Timer fired');
25}, 100);
26
27// Disable when done
28// hook.disable();AsyncLocalStorage (Recommended)#
1import { AsyncLocalStorage } from 'node:async_hooks';
2
3// Create storage instance
4const asyncLocalStorage = new AsyncLocalStorage();
5
6// Run with context
7function handleRequest(req, res) {
8 const context = {
9 requestId: crypto.randomUUID(),
10 userId: req.user?.id,
11 startTime: Date.now(),
12 };
13
14 asyncLocalStorage.run(context, () => {
15 processRequest(req, res);
16 });
17}
18
19// Access context anywhere in the call chain
20function processRequest(req, res) {
21 const context = asyncLocalStorage.getStore();
22 console.log(`Processing request ${context.requestId}`);
23
24 // Context is available in async callbacks too
25 setTimeout(() => {
26 const ctx = asyncLocalStorage.getStore();
27 console.log(`Still have context: ${ctx.requestId}`);
28 }, 100);
29}Request Tracing#
1import { AsyncLocalStorage } from 'node:async_hooks';
2import http from 'node:http';
3
4const requestContext = new AsyncLocalStorage();
5
6// Logger that includes request context
7function log(message) {
8 const ctx = requestContext.getStore();
9 const prefix = ctx ? `[${ctx.requestId}]` : '[no-context]';
10 console.log(`${prefix} ${message}`);
11}
12
13// Middleware to set up context
14function contextMiddleware(req, res, next) {
15 const context = {
16 requestId: req.headers['x-request-id'] || crypto.randomUUID(),
17 method: req.method,
18 url: req.url,
19 startTime: Date.now(),
20 };
21
22 requestContext.run(context, () => {
23 res.on('finish', () => {
24 const ctx = requestContext.getStore();
25 const duration = Date.now() - ctx.startTime;
26 log(`${ctx.method} ${ctx.url} completed in ${duration}ms`);
27 });
28
29 next();
30 });
31}
32
33// Usage in route handlers
34async function getUser(id) {
35 log(`Fetching user ${id}`);
36 // Context preserved through async operations
37 const user = await database.findUser(id);
38 log(`Found user: ${user.name}`);
39 return user;
40}Execution Context Tracking#
1import async_hooks from 'node:async_hooks';
2import fs from 'node:fs';
3
4// Track execution contexts
5const contexts = new Map();
6let currentId = -1;
7
8const hook = async_hooks.createHook({
9 init(asyncId, type, triggerAsyncId) {
10 // Inherit context from trigger
11 if (contexts.has(triggerAsyncId)) {
12 contexts.set(asyncId, contexts.get(triggerAsyncId));
13 }
14 },
15 before(asyncId) {
16 currentId = asyncId;
17 },
18 after() {
19 currentId = -1;
20 },
21 destroy(asyncId) {
22 contexts.delete(asyncId);
23 },
24});
25
26hook.enable();
27
28function setContext(ctx) {
29 const asyncId = async_hooks.executionAsyncId();
30 contexts.set(asyncId, ctx);
31}
32
33function getContext() {
34 const asyncId = async_hooks.executionAsyncId();
35 return contexts.get(asyncId);
36}
37
38// Usage
39setContext({ user: 'john', transaction: 'tx-123' });
40
41setTimeout(() => {
42 console.log(getContext()); // { user: 'john', transaction: 'tx-123' }
43}, 100);Performance Monitoring#
1import { AsyncLocalStorage } from 'node:async_hooks';
2
3const performanceContext = new AsyncLocalStorage();
4
5class PerformanceTracker {
6 constructor() {
7 this.spans = [];
8 }
9
10 startSpan(name) {
11 const span = {
12 name,
13 startTime: process.hrtime.bigint(),
14 endTime: null,
15 children: [],
16 };
17 this.spans.push(span);
18 return span;
19 }
20
21 endSpan(span) {
22 span.endTime = process.hrtime.bigint();
23 span.duration = Number(span.endTime - span.startTime) / 1e6; // ms
24 }
25
26 getReport() {
27 return this.spans.map((s) => ({
28 name: s.name,
29 duration: `${s.duration?.toFixed(2)}ms`,
30 }));
31 }
32}
33
34async function trackedOperation(name, fn) {
35 const tracker = performanceContext.getStore();
36 if (!tracker) return fn();
37
38 const span = tracker.startSpan(name);
39 try {
40 return await fn();
41 } finally {
42 tracker.endSpan(span);
43 }
44}
45
46// Usage
47async function handleRequest(req) {
48 const tracker = new PerformanceTracker();
49
50 return performanceContext.run(tracker, async () => {
51 await trackedOperation('validateRequest', () => validate(req));
52 await trackedOperation('fetchData', () => fetchFromDB());
53 await trackedOperation('processData', () => process());
54
55 console.log(tracker.getReport());
56 });
57}Database Transaction Context#
1import { AsyncLocalStorage } from 'node:async_hooks';
2
3const transactionContext = new AsyncLocalStorage();
4
5class TransactionManager {
6 async runInTransaction(fn) {
7 const connection = await pool.getConnection();
8 await connection.beginTransaction();
9
10 try {
11 const result = await transactionContext.run(connection, fn);
12 await connection.commit();
13 return result;
14 } catch (error) {
15 await connection.rollback();
16 throw error;
17 } finally {
18 connection.release();
19 }
20 }
21}
22
23// Repository that uses transaction context
24class UserRepository {
25 async create(userData) {
26 const conn = transactionContext.getStore() || pool;
27 return conn.query('INSERT INTO users SET ?', userData);
28 }
29
30 async update(id, data) {
31 const conn = transactionContext.getStore() || pool;
32 return conn.query('UPDATE users SET ? WHERE id = ?', [data, id]);
33 }
34}
35
36// Usage
37const txManager = new TransactionManager();
38const userRepo = new UserRepository();
39
40await txManager.runInTransaction(async () => {
41 await userRepo.create({ name: 'John' });
42 await userRepo.update(1, { status: 'active' });
43 // Both operations use same transaction
44});Error Context Propagation#
1import { AsyncLocalStorage } from 'node:async_hooks';
2
3const errorContext = new AsyncLocalStorage();
4
5class ContextualError extends Error {
6 constructor(message) {
7 super(message);
8 this.context = errorContext.getStore() || {};
9 }
10}
11
12function withErrorContext(context, fn) {
13 const current = errorContext.getStore() || {};
14 return errorContext.run({ ...current, ...context }, fn);
15}
16
17// Usage
18async function processOrder(orderId) {
19 return withErrorContext({ orderId }, async () => {
20 const order = await fetchOrder(orderId);
21
22 return withErrorContext({ customerId: order.customerId }, async () => {
23 // If error occurs here, it includes orderId and customerId
24 await processPayment(order);
25 });
26 });
27}
28
29// Error handler
30process.on('uncaughtException', (error) => {
31 if (error instanceof ContextualError) {
32 console.error('Error with context:', {
33 message: error.message,
34 context: error.context,
35 stack: error.stack,
36 });
37 }
38});Resource Cleanup Tracking#
1import async_hooks from 'node:async_hooks';
2
3const activeResources = new Map();
4
5const hook = async_hooks.createHook({
6 init(asyncId, type, triggerAsyncId, resource) {
7 activeResources.set(asyncId, {
8 type,
9 triggerAsyncId,
10 createdAt: Date.now(),
11 stack: new Error().stack,
12 });
13 },
14 destroy(asyncId) {
15 activeResources.delete(asyncId);
16 },
17});
18
19hook.enable();
20
21// Check for leaks periodically
22setInterval(() => {
23 const now = Date.now();
24 const leaks = [];
25
26 for (const [id, info] of activeResources) {
27 const age = now - info.createdAt;
28 if (age > 30000) {
29 // 30 seconds
30 leaks.push({
31 asyncId: id,
32 type: info.type,
33 age: `${(age / 1000).toFixed(1)}s`,
34 });
35 }
36 }
37
38 if (leaks.length > 0) {
39 console.warn('Potential resource leaks:', leaks);
40 }
41}, 10000);Logging Context#
1import { AsyncLocalStorage } from 'node:async_hooks';
2
3const logContext = new AsyncLocalStorage();
4
5class Logger {
6 static log(level, message, meta = {}) {
7 const ctx = logContext.getStore() || {};
8 const logEntry = {
9 timestamp: new Date().toISOString(),
10 level,
11 message,
12 ...ctx,
13 ...meta,
14 };
15 console.log(JSON.stringify(logEntry));
16 }
17
18 static info(message, meta) {
19 this.log('info', message, meta);
20 }
21
22 static error(message, meta) {
23 this.log('error', message, meta);
24 }
25
26 static withContext(context, fn) {
27 const current = logContext.getStore() || {};
28 return logContext.run({ ...current, ...context }, fn);
29 }
30}
31
32// Usage
33async function handleRequest(req) {
34 return Logger.withContext(
35 { requestId: req.id, userId: req.user?.id },
36 async () => {
37 Logger.info('Request started');
38
39 await Logger.withContext({ operation: 'fetchUser' }, async () => {
40 Logger.info('Fetching user data');
41 // Logs include requestId, userId, and operation
42 });
43
44 Logger.info('Request completed');
45 }
46 );
47}Best Practices#
AsyncLocalStorage:
✓ Prefer over raw async_hooks
✓ Use for request context
✓ Use for transaction scoping
✓ Use for logging context
Performance:
✓ Minimize hook callbacks
✓ Avoid heavy processing in hooks
✓ Disable when not needed
✓ Use sampling for tracing
Patterns:
✓ Request ID propagation
✓ Transaction management
✓ Performance tracing
✓ Error context enrichment
Avoid:
✗ Storing large objects in context
✗ Modifying context after creation
✗ Relying on context in cleanup
✗ Circular references in context
Conclusion#
The async_hooks module enables powerful async context tracking in Node.js. Use AsyncLocalStorage for request tracing, transaction management, and contextual logging. It automatically propagates context through async boundaries without manual passing. For advanced use cases like resource tracking and debugging, use the lower-level createHook API. Remember that async_hooks has performance overhead, so use it judiciously in production.