Back to Blog
Node.jsasync_hooksAsync ContextDebugging

Node.js async_hooks Module Guide

Master Node.js async_hooks for tracking asynchronous resources and building context-aware applications.

B
Bootspring Team
Engineering
March 28, 2019
6 min read

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();
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.

Share this article

Help spread the word about Bootspring