Skip to content

Node.js Async Hooks — Complete Guide to Async Lifecycle Tracking

DodaTech Updated 2026-06-28 4 min read

In this tutorial, you will learn about Node.js Async Hooks. We cover key concepts, practical examples, and best practices to help you master this topic.

Node.js async hooks API tracks the lifecycle of asynchronous resources, providing callbacks for init, before, after, destroy, and promiseResolve events across the entire application.

What You'll Learn

By the end of this tutorial, you'll use async hooks to monitor async operations, implement context propagation (like AsyncLocalStorage), track resource leaks, and build diagnostic tools.

Why Async Hooks Matter

Debugging async code is notoriously difficult. Async hooks give you visibility into every async operation, enabling context-aware logging, request tracing, and resource leak detection.

Real-World Use

A tracing system uses async hooks to assign every async operation a trace ID, propagating it through promises, timers, and I/O callbacks so logs from a single request are correlated.

Async Hooks Path

flowchart LR
  A[Async Patterns] --> B[Async Hooks]
  B --> C[Error Handling]
  C --> D[Debugging]
  D --> E[Profiling]
  B --> F{You Are Here}
  style F fill:#f90,color:#fff

Creating an AsyncHook

The createHook method registers callbacks for async resource lifecycle events.

const asyncHooks = require("node:async_hooks");
const fs = require("node:fs");
const hook = asyncHooks.createHook({
  init(asyncId, type, triggerAsyncId) {
    fs.writeSync(1, `Init: ${type} id=${asyncId} trigger=${triggerAsyncId}\n`);
  },
  before(asyncId) {
    fs.writeSync(1, `Before: id=${asyncId}\n`);
  },
  after(asyncId) {
    fs.writeSync(1, `After: id=${asyncId}\n`);
  },
  destroy(asyncId) {
    fs.writeSync(1, `Destroy: id=${asyncId}\n`);
  },
});
hook.enable();
setTimeout(() => {}, 100);

AsyncLocalStorage Context

AsyncLocalStorage provides context propagation without passing arguments manually through async chains.

const { AsyncLocalStorage } = require("node:async_hooks");
const als = new AsyncLocalStorage();
function log(message) {
  const store = als.getStore();
  console.log(`[${store?.requestId ?? "unknown"}] ${message}`);
}
app.use((req, res, next) => {
  als.run({ requestId: req.id }, () => {
    log("Request started");
    next();
  });
});

Tracking Async Resource Types

Different async operations have distinct types: TIMERWRAP, PROMISE, FSREQCALLBACK, TCPWRAP, etc.

const asyncHooks = require("node:async_hooks");
const types = new Map();
const hook = asyncHooks.createHook({
  init(asyncId, type) {
    types.set(type, (types.get(type) || 0) + 1);
  },
  destroy(asyncId, type) {
    // tracking cleanup
  },
});
hook.enable();
setInterval(() => {
  console.log("Active async resources by type:");
  types.forEach((count, type) => console.log(`  ${type}: ${count}`));
}, 5000);

Enabling and Disabling Hooks

Hooks can be enabled and disabled at runtime. Disable hooks when not needed to avoid performance overhead.

const asyncHooks = require("node:async_hooks");
const hook = asyncHooks.createHook({
  init() {},
  before() {},
  after() {},
  destroy() {},
});
hook.enable();
console.log("Hooks enabled, tracking async operations...");
setTimeout(() => {
  hook.disable();
  console.log("Hooks disabled, no longer tracking");
}, 3000);

Execution Async ID

The executionAsyncId function returns the ID of the currently executing async resource.

const { executionAsyncId, triggerAsyncId } = require("node:async_hooks");
console.log("Current execution async ID:", executionAsyncId());
console.log("Trigger async ID:", triggerAsyncId());
setTimeout(() => {
  console.log("Inside timeout - execution ID:", executionAsyncId());
  console.log("Inside timeout - trigger ID:", triggerAsyncId());
}, 100);

Common Mistakes

1. Using console.log Inside Async Hook Callbacks

console.log is async and can trigger init hooks recursively. Use fs.writeSync(1, ...) for synchronous output.

2. Performance Impact of Async Hooks

Async hooks add overhead to every async operation. Enable only during debugging or diagnostics.

3. Not Disabling Hooks After Use

Active hooks slow down the application permanently. Disable hooks when the diagnostic session ends.

4. Modifying Global State in Hooks

Async hook callbacks should be side-effect free. Avoid modifying shared state synchronously.

5. Confusing executionAsyncId and triggerAsyncId

executionAsyncId is the current resource. triggerAsyncId is the resource that created the current one.

Practice Questions

1. What are the four main async hook callbacks?

init, before, after, and destroy. promiseResolve is an additional optional callback.

2. What is the difference between executionAsyncId and triggerAsyncId?

executionAsyncId identifies the currently executing resource. triggerAsyncId identifies the resource that created it.

3. Why should you avoid console.log in hook callbacks?

console.log is async and can cause infinite Recursion by triggering init for new async resources.

4. What is AsyncLocalStorage used for?

Propagating context (like request IDs) through async chains without passing it explicitly.

5. Challenge: Build a simple context propagator using async hooks.

const { createHook, executionAsyncId } = require("node:async_hooks");
const contexts = new Map();
const hook = createHook({
  init(asyncId, type, triggerAsyncId) {
    if (contexts.has(triggerAsyncId)) {
      contexts.set(asyncId, contexts.get(triggerAsyncId));
    }
  },
  destroy(asyncId) { contexts.delete(asyncId); },
});
hook.enable();
module.exports = { contexts, executionAsyncId };

FAQ

Are async hooks stable for production use?

async_hooks is still experimental (Stability 1). Use with caution. AsyncLocalStorage has higher stability (Stability 2).

Does async hooks work with all async primitives?

Most. Promises, timers, I/O, and async-await are tracked. Some internal resources may not trigger hooks.

What is the performance cost of async hooks?

Significant. Each async operation triggers up to 4 callbacks. Not recommended for production use in hot paths.

Can I use async hooks in production?

AsyncLocalStorage is production-ready. Low-level createHook is better suited for debugging and diagnostics.

How does AsyncLocalStorage compare to continuation-local-storage?

AsyncLocalStorage is the official replacement built into Node.js. cls-hooked is a third-party package.

Mini Project: Request Tracing Middleware

Build Express middleware that assigns unique IDs to every request using AsyncLocalStorage.

const { AsyncLocalStorage } = require("node:async_hooks");
const crypto = require("node:crypto");
const als = new AsyncLocalStorage();
function requestTracker(req, res, next) {
  const requestId = crypto.randomUUID();
  als.run({ requestId, startTime: Date.now() }, () => {
    res.on("finish", () => {
      const store = als.getStore();
      const duration = Date.now() - store.startTime;
      console.log(`[${store.requestId}] ${req.method} ${req.url} ${res.statusCode} ${duration}ms`);
    });
    next();
  });
}
module.exports = { als, requestTracker };

What's Next

Node.js Error Handling Node.js Debugging Node.js Profiling

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro