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.