0Pricing
Node.js Backend Development Bootcamp · Lección

Propagación de contexto con AsyncLocalStorage

Transporte el estado asociado a una solicitud entre límites asíncronos sin propagarlo manualmente mediante AsyncLocalStorage.

Propagación de contexto con AsyncLocalStorage es una lección gratuita de Node.js Backend Development Bootcamp en CoddyKit. Esta es la lección 4 de 4. Puedes leer la lección completa abajo gratuitamente — luego la practicas en el navegador con un editor de código integrado y un tutor de IA 24/7. Forma parte de la ruta de aprendizaje de Node.js Backend Development Bootcamp, y tu progreso se sincroniza en la web y la app de CoddyKit. El curso de Node.js Backend Development Bootcamp incluye 4 lecciones en total.

Partes de esta lección aún no han sido traducidas y se muestran en inglés.

The Prop-Drilling Problem

In a backend service, request-scoped data such as a request ID, the authenticated user, or a tenant ID is needed deep inside your call stack: in repositories, loggers, and outbound HTTP clients.

The naive fix is prop drilling — threading a ctx argument through every function:

  • Every function signature gets polluted with a ctx parameter.
  • One missed hand-off and a downstream call loses context.
  • Library code you don't own can't receive your ctx at all.

We need a way to carry per-request state implicitly, surviving every await and callback. That is exactly what AsyncLocalStorage provides.

async function handler(ctx, req) {
  const user = await loadUser(ctx, req.userId);
  return await renderPage(ctx, user);
}

async function loadUser(ctx, id) {
  log(ctx, 'loading user');         // ctx passed again
  return await db.find(ctx, id);    // and again...
}

// Every layer must accept and forward ctx by hand.

What AsyncLocalStorage Is

AsyncLocalStorage lives in the built-in node:async_hooks module. Think of it as thread-local storage for the async world: a store that stays attached to a logical chain of asynchronous operations.

You call als.run(store, callback) to establish a store, then anywhere inside that callback — no matter how many awaits, Promise chains, setTimeouts, or event emitters deep — als.getStore() returns the same store.

  • Each concurrent request gets its own isolated store.
  • No globals, no race conditions between requests.
  • Powered by Node's async context tracking under the hood.
import { AsyncLocalStorage } from 'node:async_hooks';

const als = new AsyncLocalStorage();

als.run({ requestId: 'abc-123' }, () => {
  setTimeout(() => {
    const store = als.getStore();
    console.log(store.requestId); // 'abc-123'
  }, 10);
});

run() Establishes a Context

The core API is als.run(store, fn, ...args). It executes fn synchronously, but binds store to the entire async tree spawned from it.

  • The store can be any value — most often a plain object or a Map.
  • run() returns whatever fn returns (including a Promise).
  • Outside the callback, getStore() returns undefined.

This snippet shows that the store survives across an await on a real timer.

import { AsyncLocalStorage } from 'node:async_hooks';
import { setTimeout as sleep } from 'node:timers/promises';

const als = new AsyncLocalStorage();

async function deep() {
  await sleep(5);
  return als.getStore()?.requestId;
}

await als.run({ requestId: 'r-42' }, async () => {
  const id = await deep();
  console.log('inside run:', id);   // 'r-42'
});

console.log('outside run:', als.getStore()); // undefined

Wiring It Into an HTTP Server

The pattern in a real server: wrap the per-request work in als.run() at the very edge, seeding the store with a fresh request ID. Everything downstream can then read it.

Below uses only the built-in node:http module, so there is no framework dependency. Each incoming request gets its own store, isolated from concurrent requests.

import http from 'node:http';
import { randomUUID } from 'node:crypto';
import { AsyncLocalStorage } from 'node:async_hooks';

const als = new AsyncLocalStorage();

function currentRequestId() {
  return als.getStore()?.requestId ?? 'no-context';
}

const server = http.createServer((req, res) => {
  als.run({ requestId: randomUUID() }, async () => {
    // deep call needs no ctx argument
    await Promise.resolve();
    res.end('request id: ' + currentRequestId());
  });
});

server.listen(3000, () => console.log('listening on 3000'));

Express Middleware Pattern

In Express the idiomatic place to call als.run() is a middleware mounted first. It must call next() inside the callback so the rest of the chain inherits the store.

  • Read an incoming x-request-id header if a proxy or upstream service supplied one; otherwise generate a fresh UUID.
  • Every later handler, service, and logger can read the store without receiving it as an argument.
import { AsyncLocalStorage } from 'node:async_hooks';
import { randomUUID } from 'node:crypto';

export const als = new AsyncLocalStorage();

export function contextMiddleware(req, res, next) {
  const store = {
    requestId: req.headers['x-request-id'] || randomUUID(),
    startedAt: Date.now(),
  };
  als.run(store, () => next()); // next() runs inside the context
}

export function getStore() {
  const store = als.getStore();
  if (!store) throw new Error('No request context');
  return store;
}

Context-Aware Structured Logging

The biggest payoff: a logger that automatically stamps every line with the request ID — no caller ever passes it.

The logger reads the store itself. If there is no active context (e.g. startup code), it degrades gracefully.

import { AsyncLocalStorage } from 'node:async_hooks';

const als = new AsyncLocalStorage();

function log(level, msg, extra = {}) {
  const store = als.getStore();
  const line = {
    ts: new Date().toISOString(),
    level,
    msg,
    requestId: store?.requestId ?? null,
    ...extra,
  };
  console.log(JSON.stringify(line));
}

als.run({ requestId: 'req-7' }, () => {
  log('info', 'user fetched', { userId: 99 });
});

log('warn', 'no request context here');

Mutating the Store Mid-Request

Because the store is a reference (object or Map), you can enrich it after authentication resolves. Later log lines automatically pick up the new fields.

  • Seed with minimal data at the edge (request ID).
  • After auth middleware runs, attach userId and tenantId to the same store object.
  • Prefer a Map if you want a clear key-based API; a plain object is fine and slightly faster.
import { AsyncLocalStorage } from 'node:async_hooks';

const als = new AsyncLocalStorage();

function set(key, value) {
  const store = als.getStore();
  if (store) store.set(key, value);
}
function get(key) {
  return als.getStore()?.get(key);
}

als.run(new Map([['requestId', 'r-1']]), () => {
  // ... later, after authenticating:
  set('userId', 42);
  set('tenantId', 'acme');
  console.log(get('requestId'), get('userId'), get('tenantId'));
});

enterWith vs run

There are two ways to set a store:

  • als.run(store, fn) — scopes the store to fn and its async descendants. When fn settles, the context is gone. Prefer this.
  • als.enterWith(store) — sets the store for the current sync execution and everything after it in the same async resource, with no automatic exit.

enterWith is sharp: if you call it in a long-lived async resource it can leak into unrelated later work. Use run unless you have a specific reason (e.g. you cannot wrap a callback).

import { AsyncLocalStorage } from 'node:async_hooks';

const als = new AsyncLocalStorage();

// Scoped and safe — context ends with the callback:
als.run({ id: 'A' }, () => {
  console.log(als.getStore().id); // 'A'
});
console.log(als.getStore());      // undefined

// enterWith persists with no clear boundary — easy to leak:
als.enterWith({ id: 'B' });
console.log(als.getStore().id);   // 'B' (and stays set!)

Where Context Can Break

AsyncLocalStorage follows native promises, async/await, timers, and most event emitters. But context can be lost in a few situations:

  • Work scheduled before run() — e.g. a connection pool or queue created at startup runs its callbacks outside any request store.
  • Some older libraries that pool or reuse resources across requests can carry a stale store.
  • Manually detached callbacks stored in a global array and invoked later.

The fix when integrating such code is als.bind(fn) (or AsyncResource.bind), which snapshots the current context and re-applies it whenever the function is later called.

import { AsyncLocalStorage } from 'node:async_hooks';

const als = new AsyncLocalStorage();
const queue = [];

als.run({ requestId: 'r-9' }, () => {
  // bind captures the current store for later execution
  queue.push(als.bind(() => {
    console.log('later:', als.getStore()?.requestId);
  }));
});

// Runs outside the run() callback, but context is preserved:
queue.forEach((fn) => fn()); // later: r-9

A Reusable Context Module

In practice you centralize the store in one small module so the rest of the codebase only imports helpers — never touching the AsyncLocalStorage instance directly.

This keeps the API tidy: runWithContext() at the edge, requestId() / getUser() everywhere else.

import { AsyncLocalStorage } from 'node:async_hooks';
import { randomUUID } from 'node:crypto';

const als = new AsyncLocalStorage();

export function runWithContext(seed, fn) {
  const store = { requestId: randomUUID(), ...seed };
  return als.run(store, fn);
}

export function context() {
  return als.getStore() ?? null;
}

export function requestId() {
  return context()?.requestId ?? null;
}

export function setUser(user) {
  const store = als.getStore();
  if (store) store.user = user;
}

Performance & Good Hygiene

AsyncLocalStorage in modern Node (v16+) is backed by an efficient native implementation and the overhead is small — acceptable for virtually all web workloads. Still, follow some hygiene:

  • Use one long-lived AsyncLocalStorage instance per concern, not one per request.
  • Keep the store small; it is request-scoped state, not a cache.
  • Don't store secrets you wouldn't want appearing in logs that read the store.
  • Prefer run() over enterWith() to get automatic cleanup.
  • Always handle the undefined store case for code that may run outside a request.

Quick Check

Test your understanding of how to establish request context safely.

Recap

You learned how to propagate request-scoped state across async boundaries without prop drilling:

  • AsyncLocalStorage from node:async_hooks is thread-local storage for the async world.
  • als.run(store, fn) binds a store to fn and all its async descendants; als.getStore() reads it anywhere; outside the run it is undefined.
  • Wire it once at the edge (HTTP server or first middleware), seeding a request ID, then enrich the store after auth.
  • A context-aware logger can stamp every line with the request ID automatically.
  • Prefer run() over enterWith() for automatic cleanup, and use als.bind() / AsyncResource.bind() to preserve context across detached callbacks.
  • Keep the store small, handle the no-context case, and centralize access in one module.

Preguntas frecuentes

¿La lección «Propagación de contexto con AsyncLocalStorage» es gratis?

Sí — el texto completo de «Propagación de contexto con AsyncLocalStorage» es gratis para leer aquí en la web. Para practicarla de forma interactiva (editor de código integrado y tutor de IA 24/7) y desbloquear el resto del curso de Node.js Backend Development Bootcamp, actualiza a CoddyKit PRO. El curso de Node.js Backend Development Bootcamp incluye 4 lecciones en total.

¿Qué aprenderé en «Propagación de contexto con AsyncLocalStorage»?

Transporte el estado asociado a una solicitud entre límites asíncronos sin propagarlo manualmente mediante AsyncLocalStorage. Practicas Node.js Backend Development Bootcamp con código real que ejecutas directamente en el navegador, y un tutor de IA 24/7 responde tus preguntas mientras trabajas en la lección.

¿Necesito experiencia previa para empezar Node.js Backend Development Bootcamp?

No se requiere experiencia previa. Node.js Backend Development Bootcamp en CoddyKit está estructurado para principiantes hasta estudiantes avanzados, así que puedes empezar aquí o desde el inicio y avanzar a tu ritmo. Esta es la lección 4 de 4.

¿Cuánto tiempo toma la lección «Propagación de contexto con AsyncLocalStorage»?

La mayoría de las lecciones de CoddyKit toman alrededor de 5–10 minutos. Cada una es compacta e interactiva, así que avanzas constantemente y retomas exactamente por donde dejaste en la web y la app.

¿Puedo escribir y ejecutar código en esta lección de Node.js Backend Development Bootcamp?

Sí. Cada lección de Node.js Backend Development Bootcamp incluye un editor de código integrado, así que escribes y ejecutas código real directamente en tu navegador y obtienes retroalimentación instantánea de IA — sin configuración local necesaria.

Todas las lecciones de este curso

  1. Logging estructurado con identificadores de correlación
  2. Trazas distribuidas con spans de OpenTelemetry
  3. Exposición de métricas de aplicación y método RED
  4. Propagación de contexto con AsyncLocalStorage
← Volver a Node.js Backend Development Bootcamp