Back to Blog
Node.jsasync_hooksAsyncDebugging

Node.js Async Hooks Module Guide

Master Node.js async_hooks module for tracking asynchronous operations and building context propagation.

B
Bootspring Team
Engineering
December 9, 2019
7 min read

The async_hooks module tracks the lifecycle of asynchronous resources. Here's how to use it.

Basic Usage#

1const async_hooks = require('async_hooks'); 2const fs = require('fs'); 3 4// Create hook instance 5const hook = async_hooks.createHook({ 6 init(asyncId, type, triggerAsyncId, resource) { 7 // Called when async resource is created 8 fs.writeSync( 9 1, 10 `Init: ${type}(${asyncId}) trigger: ${triggerAsyncId}\n` 11 ); 12 }, 13 14 before(asyncId) { 15 // Called before async callback runs 16 fs.writeSync(1, `Before: ${asyncId}\n`); 17 }, 18 19 after(asyncId) { 20 // Called after async callback completes 21 fs.writeSync(1, `After: ${asyncId}\n`); 22 }, 23 24 destroy(asyncId) { 25 // Called when async resource is destroyed 26 fs.writeSync(1, `Destroy: ${asyncId}\n`); 27 }, 28}); 29 30// Enable the hook 31hook.enable(); 32 33// Your async code 34setTimeout(() => { 35 console.log('Timer fired'); 36}, 100); 37 38// Disable when done 39// hook.disable();

Execution Context Tracking#

1const async_hooks = require('async_hooks'); 2 3// Track async context 4const contexts = new Map(); 5 6const hook = async_hooks.createHook({ 7 init(asyncId, type, triggerAsyncId) { 8 // Inherit context from trigger 9 if (contexts.has(triggerAsyncId)) { 10 contexts.set(asyncId, contexts.get(triggerAsyncId)); 11 } 12 }, 13 14 destroy(asyncId) { 15 contexts.delete(asyncId); 16 }, 17}); 18 19hook.enable(); 20 21// Set context for current execution 22function setContext(data) { 23 const asyncId = async_hooks.executionAsyncId(); 24 contexts.set(asyncId, data); 25} 26 27// Get context for current execution 28function getContext() { 29 const asyncId = async_hooks.executionAsyncId(); 30 return contexts.get(asyncId); 31} 32 33// Usage 34async function handleRequest(requestId) { 35 setContext({ requestId }); 36 37 await someAsyncOperation(); 38 39 const ctx = getContext(); 40 console.log('Request:', ctx.requestId); // Still has context 41}

AsyncLocalStorage#

1const { AsyncLocalStorage } = require('async_hooks'); 2 3// Create storage instance 4const requestContext = new AsyncLocalStorage(); 5 6// Middleware pattern 7function requestMiddleware(req, res, next) { 8 const context = { 9 requestId: generateId(), 10 startTime: Date.now(), 11 userId: req.userId, 12 }; 13 14 requestContext.run(context, () => { 15 next(); 16 }); 17} 18 19// Access context anywhere in async chain 20function logMessage(message) { 21 const ctx = requestContext.getStore(); 22 console.log(`[${ctx?.requestId}] ${message}`); 23} 24 25// Service function 26async function getUserData(userId) { 27 const ctx = requestContext.getStore(); 28 logMessage(`Fetching user ${userId}`); 29 30 const data = await db.findUser(userId); 31 32 logMessage(`Found user: ${data.name}`); 33 return data; 34} 35 36// Usage 37app.use(requestMiddleware); 38 39app.get('/user/:id', async (req, res) => { 40 const user = await getUserData(req.params.id); 41 res.json(user); 42});

Request Tracing#

1const { AsyncLocalStorage } = require('async_hooks'); 2const crypto = require('crypto'); 3 4class RequestTracer { 5 constructor() { 6 this.storage = new AsyncLocalStorage(); 7 } 8 9 startTrace(parentTraceId = null) { 10 const trace = { 11 traceId: parentTraceId || crypto.randomUUID(), 12 spanId: crypto.randomUUID(), 13 parentSpanId: parentTraceId ? crypto.randomUUID() : null, 14 startTime: process.hrtime.bigint(), 15 spans: [], 16 }; 17 18 return trace; 19 } 20 21 run(fn) { 22 const trace = this.startTrace(); 23 return this.storage.run(trace, fn); 24 } 25 26 getTrace() { 27 return this.storage.getStore(); 28 } 29 30 addSpan(name, fn) { 31 const trace = this.getTrace(); 32 const span = { 33 name, 34 spanId: crypto.randomUUID(), 35 startTime: process.hrtime.bigint(), 36 }; 37 38 trace?.spans.push(span); 39 40 const result = fn(); 41 42 span.endTime = process.hrtime.bigint(); 43 span.duration = Number(span.endTime - span.startTime) / 1e6; 44 45 return result; 46 } 47 48 getHeaders() { 49 const trace = this.getTrace(); 50 return { 51 'x-trace-id': trace?.traceId, 52 'x-span-id': trace?.spanId, 53 }; 54 } 55} 56 57const tracer = new RequestTracer(); 58 59// Usage 60app.use((req, res, next) => { 61 tracer.run(() => next()); 62}); 63 64async function processOrder(orderId) { 65 return tracer.addSpan('processOrder', async () => { 66 await tracer.addSpan('validateOrder', () => validate(orderId)); 67 await tracer.addSpan('chargePayment', () => charge(orderId)); 68 await tracer.addSpan('sendConfirmation', () => notify(orderId)); 69 }); 70}

Logger with Context#

1const { AsyncLocalStorage } = require('async_hooks'); 2 3class ContextLogger { 4 constructor() { 5 this.storage = new AsyncLocalStorage(); 6 } 7 8 withContext(context, fn) { 9 return this.storage.run(context, fn); 10 } 11 12 addContext(additionalContext) { 13 const current = this.storage.getStore() || {}; 14 return { ...current, ...additionalContext }; 15 } 16 17 log(level, message, data = {}) { 18 const context = this.storage.getStore() || {}; 19 const logEntry = { 20 timestamp: new Date().toISOString(), 21 level, 22 message, 23 ...context, 24 ...data, 25 }; 26 27 console.log(JSON.stringify(logEntry)); 28 } 29 30 info(message, data) { 31 this.log('info', message, data); 32 } 33 34 error(message, data) { 35 this.log('error', message, data); 36 } 37 38 warn(message, data) { 39 this.log('warn', message, data); 40 } 41} 42 43const logger = new ContextLogger(); 44 45// Usage 46app.use((req, res, next) => { 47 const context = { 48 requestId: req.headers['x-request-id'] || generateId(), 49 path: req.path, 50 method: req.method, 51 }; 52 53 logger.withContext(context, () => { 54 logger.info('Request started'); 55 next(); 56 }); 57}); 58 59// Anywhere in code 60async function processData(data) { 61 logger.info('Processing data', { dataSize: data.length }); 62 63 try { 64 const result = await transform(data); 65 logger.info('Processing complete', { resultSize: result.length }); 66 return result; 67 } catch (error) { 68 logger.error('Processing failed', { error: error.message }); 69 throw error; 70 } 71}

Database Connection Tracking#

1const { AsyncLocalStorage } = require('async_hooks'); 2 3class TransactionManager { 4 constructor() { 5 this.storage = new AsyncLocalStorage(); 6 } 7 8 async runInTransaction(fn) { 9 const connection = await pool.getConnection(); 10 11 try { 12 await connection.beginTransaction(); 13 14 const result = await this.storage.run({ connection }, fn); 15 16 await connection.commit(); 17 return result; 18 } catch (error) { 19 await connection.rollback(); 20 throw error; 21 } finally { 22 connection.release(); 23 } 24 } 25 26 getConnection() { 27 const store = this.storage.getStore(); 28 return store?.connection; 29 } 30} 31 32const txManager = new TransactionManager(); 33 34// Repository 35class UserRepository { 36 async create(userData) { 37 const conn = txManager.getConnection(); 38 if (conn) { 39 // Use transaction connection 40 return conn.query('INSERT INTO users SET ?', userData); 41 } 42 // Use pool directly 43 return pool.query('INSERT INTO users SET ?', userData); 44 } 45 46 async update(id, data) { 47 const conn = txManager.getConnection(); 48 const target = conn || pool; 49 return target.query('UPDATE users SET ? WHERE id = ?', [data, id]); 50 } 51} 52 53// Usage 54await txManager.runInTransaction(async () => { 55 const user = await userRepo.create({ name: 'Alice' }); 56 await orderRepo.create({ userId: user.id, amount: 100 }); 57 // Both use same connection, auto-rollback on error 58});

Performance Monitoring#

1const async_hooks = require('async_hooks'); 2 3class AsyncMonitor { 4 constructor() { 5 this.resources = new Map(); 6 this.stats = { 7 created: 0, 8 destroyed: 0, 9 active: 0, 10 byType: {}, 11 }; 12 13 this.hook = async_hooks.createHook({ 14 init: this.onInit.bind(this), 15 destroy: this.onDestroy.bind(this), 16 }); 17 } 18 19 onInit(asyncId, type, triggerAsyncId) { 20 this.resources.set(asyncId, { 21 type, 22 triggerAsyncId, 23 createdAt: Date.now(), 24 }); 25 26 this.stats.created++; 27 this.stats.active++; 28 this.stats.byType[type] = (this.stats.byType[type] || 0) + 1; 29 } 30 31 onDestroy(asyncId) { 32 const resource = this.resources.get(asyncId); 33 if (resource) { 34 this.stats.destroyed++; 35 this.stats.active--; 36 this.stats.byType[resource.type]--; 37 this.resources.delete(asyncId); 38 } 39 } 40 41 enable() { 42 this.hook.enable(); 43 } 44 45 disable() { 46 this.hook.disable(); 47 } 48 49 getStats() { 50 return { 51 ...this.stats, 52 pending: this.stats.active, 53 }; 54 } 55 56 getActiveResources() { 57 return Array.from(this.resources.entries()).map(([id, resource]) => ({ 58 asyncId: id, 59 ...resource, 60 age: Date.now() - resource.createdAt, 61 })); 62 } 63} 64 65const monitor = new AsyncMonitor(); 66monitor.enable(); 67 68// Check stats periodically 69setInterval(() => { 70 console.log('Async stats:', monitor.getStats()); 71}, 5000);

Error Tracking#

1const { AsyncLocalStorage } = require('async_hooks'); 2 3class ErrorTracker { 4 constructor() { 5 this.storage = new AsyncLocalStorage(); 6 } 7 8 withErrorContext(context, fn) { 9 return this.storage.run( 10 { 11 ...context, 12 breadcrumbs: [], 13 }, 14 fn 15 ); 16 } 17 18 addBreadcrumb(message, data = {}) { 19 const store = this.storage.getStore(); 20 if (store) { 21 store.breadcrumbs.push({ 22 timestamp: Date.now(), 23 message, 24 data, 25 }); 26 } 27 } 28 29 captureException(error) { 30 const store = this.storage.getStore() || {}; 31 return { 32 error: { 33 name: error.name, 34 message: error.message, 35 stack: error.stack, 36 }, 37 context: { 38 requestId: store.requestId, 39 userId: store.userId, 40 path: store.path, 41 }, 42 breadcrumbs: store.breadcrumbs || [], 43 }; 44 } 45} 46 47const errorTracker = new ErrorTracker(); 48 49// Middleware 50app.use((req, res, next) => { 51 errorTracker.withErrorContext( 52 { 53 requestId: generateId(), 54 userId: req.userId, 55 path: req.path, 56 }, 57 () => next() 58 ); 59}); 60 61// Add breadcrumbs 62async function checkout(cart) { 63 errorTracker.addBreadcrumb('Starting checkout', { items: cart.length }); 64 65 const total = calculateTotal(cart); 66 errorTracker.addBreadcrumb('Calculated total', { total }); 67 68 try { 69 const result = await processPayment(total); 70 errorTracker.addBreadcrumb('Payment processed'); 71 return result; 72 } catch (error) { 73 const report = errorTracker.captureException(error); 74 sendToErrorService(report); 75 throw error; 76 } 77}

Best Practices#

Usage: ✓ Prefer AsyncLocalStorage over raw hooks ✓ Use for request context propagation ✓ Use for transaction management ✓ Use for distributed tracing Performance: ✓ Minimize work in hook callbacks ✓ Use writeSync for debugging ✓ Clean up resources in destroy ✓ Disable hooks when not needed Patterns: ✓ Logger with request context ✓ Transaction scoping ✓ Error tracking with breadcrumbs ✓ Performance monitoring Avoid: ✗ Storing large objects ✗ Synchronous operations in hooks ✗ Memory leaks from missing cleanup ✗ Using for simple use cases

Conclusion#

The async_hooks module enables tracking asynchronous operations in Node.js. Use AsyncLocalStorage for request context propagation, logging correlation, and transaction management. Raw async hooks are useful for debugging, profiling, and building custom instrumentation. Always clean up resources properly to prevent memory leaks.

Share this article

Help spread the word about Bootspring