Operações assíncronas criam recursos internos para timers, Promises, sockets, arquivos e outras tarefas. O módulo Async Hooks no Node.js permite observar o ciclo de vida desses recursos e relacionar uma operação atual com o recurso que a originou.
Essa capacidade é usada por ferramentas de tracing, profilers, detectores de vazamento e mecanismos de contexto. Entretanto, a API é de baixo nível e pode adicionar overhead significativo. Para compartilhar dados entre callbacks de uma requisição, AsyncLocalStorage normalmente oferece uma interface mais segura e simples.
Neste guia, você aprenderá conceitos como asyncId, triggerAsyncId, init, before, after, destroy e promiseResolve, além de cuidados com logging, memória, recursão e uso em produção.
O que é node:async_hooks?
node:async_hooks é um módulo nativo que acompanha recursos assíncronos criados pelo runtime. Um hook pode receber notificações quando o recurso é inicializado, antes e depois de seu callback, quando é destruído e quando uma Promise é resolvida.
A documentação oficial de Async Hooks descreve a API e suas limitações. Para casos de contexto por requisição, consulte AsyncLocalStorage no Node.js. Para compreender a execução, veja Event Loop no Node.js.
Identificadores assíncronos
Cada recurso acompanhado recebe um asyncId. O triggerAsyncId identifica o recurso que causou sua criação. Essa relação forma uma árvore aproximada de causalidade.
const asyncHooks = require('node:async_hooks');
console.log({
execution: asyncHooks.executionAsyncId(),
trigger: asyncHooks.triggerAsyncId()
});executionAsyncId() retorna o identificador do contexto em execução naquele instante. O valor não deve ser tratado como identificador permanente de uma requisição.
Criando um hook
const hook = asyncHooks.createHook({
init(asyncId, type, triggerAsyncId, resource) {
// Recurso criado
},
before(asyncId) {
// Antes do callback
},
after(asyncId) {
// Depois do callback
},
destroy(asyncId) {
// Recurso destruído
},
promiseResolve(asyncId) {
// Promise resolvida
}
});O hook só começa a receber eventos depois de ser habilitado:
hook.enable();Para interromper:
hook.disable();O callback init
init executa quando um recurso assíncrono é criado:
init(asyncId, type, triggerAsyncId) {
resources.set(asyncId, {
type,
triggerAsyncId,
createdAt: Date.now()
});
}O campo type pode indicar TIMERWRAP, PROMISE, TCPWRAP e outros tipos internos, dependendo da versão. Não crie regras frágeis baseadas em tipos não documentados como estáveis.
before e after
Esses callbacks envolvem a execução do callback associado ao recurso:
before(asyncId) {
const resource = resources.get(asyncId);
if (resource) {
resource.startedAt = performance.now();
}
},
after(asyncId) {
const resource = resources.get(asyncId);
if (resource?.startedAt) {
resource.duration =
performance.now() - resource.startedAt;
}
}Uma operação pode ter vários callbacks. Não assuma que before e after representam todo o tempo de vida do recurso.
destroy e limpeza
destroy(asyncId) {
resources.delete(asyncId);
}Sem limpeza, o mapa cresce continuamente. Alguns recursos podem ter ciclos longos, e a entrega de destroy depende do tipo e do garbage collector. Defina limites e mecanismos adicionais de expiração quando armazenar informações.
promiseResolve
Esse callback é chamado quando a função resolve de uma Promise é executada:
promiseResolve(asyncId) {
const item = resources.get(asyncId);
if (item) {
item.resolvedAt = performance.now();
}
}Isso não significa necessariamente que os handlers then() já foram executados.
Por que console.log pode ser perigoso?
Logging dentro de hooks pode criar novas operações assíncronas, que geram novos eventos e causam recursão. A documentação recomenda escrita síncrona controlada para diagnóstico simples:
const fs = require('node:fs');
function debug(message) {
fs.writeSync(1, `${message}\n`);
}Mesmo assim, produzir uma linha por evento pode gerar volume enorme e bloquear o processo. Use apenas em testes curtos.
Exemplo de árvore causal
const resources = new Map();
const hook = asyncHooks.createHook({
init(asyncId, type, triggerAsyncId) {
resources.set(asyncId, {
type,
parent: triggerAsyncId
});
},
destroy(asyncId) {
resources.delete(asyncId);
}
});
hook.enable();Essa estrutura pode ajudar a investigar quais operações criaram timers ou Promises. Em produção, acrescente limite de tamanho e amostragem.
AsyncResource
Bibliotecas que criam filas ou pools personalizados podem usar AsyncResource para preservar a associação de contexto:
const { AsyncResource } = require('node:async_hooks');
class TaskResource extends AsyncResource {
run(callback, value) {
return this.runInAsyncScope(callback, null, value);
}
close() {
this.emitDestroy();
}
}runInAsyncScope() executa o callback no escopo do recurso. Isso permite que AsyncLocalStorage e instrumentações reconheçam a continuidade correta.
Fila personalizada
class TaskQueue {
constructor() {
this.items = [];
}
add(callback, payload) {
const resource = new TaskResource('TaskQueueItem');
this.items.push({ callback, payload, resource });
}
processNext() {
const item = this.items.shift();
if (!item) return;
try {
item.resource.run(item.callback, item.payload);
} finally {
item.resource.close();
}
}
}Sem AsyncResource, o callback pode executar associado ao contexto do worker da fila, e não ao contexto que adicionou a tarefa.
AsyncLocalStorage é preferível para contexto
Para armazenar request ID, usuário autenticado ou trace ID, use AsyncLocalStorage:
const { AsyncLocalStorage } = require('node:async_hooks');
const storage = new AsyncLocalStorage();
storage.run({ requestId }, () => {
handleRequest();
});AsyncLocalStorage já utiliza mecanismos do runtime e evita a necessidade de construir mapas por asyncId manualmente.
Encontrando vazamentos
Um hook pode contar recursos ativos por tipo:
const counts = new Map();
const typesById = new Map();
const hook = asyncHooks.createHook({
init(asyncId, type) {
typesById.set(asyncId, type);
counts.set(type, (counts.get(type) || 0) + 1);
},
destroy(asyncId) {
const type = typesById.get(asyncId);
if (!type) return;
counts.set(type, counts.get(type) - 1);
typesById.delete(asyncId);
}
});Uma quantidade crescente pode indicar recursos mantidos, mas também pode refletir carga maior ou ciclos legítimos. Compare períodos semelhantes.
Overhead
Hooks são chamados em alto volume. Criar objetos, stacks e logs em cada init pode reduzir throughput. Faça benchmark com e sem instrumentação. Prefira coleta agregada e amostragem.
Ferramentas maduras costumam ativar apenas o necessário e otimizar caminhos críticos. Evite habilitar um hook detalhado permanentemente apenas por conveniência.
Capturando stack traces
Uma stack no init ajuda a localizar a origem, mas é cara:
init(asyncId, type) {
if (!shouldSample(type)) return;
resources.set(asyncId, {
type,
stack: new Error().stack
});
}Use amostragem pequena e limite de duração para investigações. Remova stacks depois do diagnóstico.
Integração com Diagnostics Channel
Uma biblioteca pode usar AsyncResource para preservar contexto e Diagnostics Channel para publicar eventos. Essas APIs têm responsabilidades complementares: uma define causalidade assíncrona e a outra cria pontos de observação.
Veja Diagnostics Channel no Node.js para canais e assinantes.
Testando contexto
test('preserva contexto na fila', async () => {
const result = await new Promise(resolve => {
storage.run({ requestId: 'abc' }, () => {
queue.add(() => {
resolve(storage.getStore()?.requestId);
});
queue.processNext();
});
});
assert.equal(result, 'abc');
});Teste caminhos de erro, cancelamento e reutilização de workers.
Erros comuns
- Usar console.log dentro do hook: pode criar recursão assíncrona.
- Guardar tudo em Map: memória cresce sem limites.
- Capturar stack em todo recurso: desempenho cai.
- Tratar asyncId como request ID: uma requisição cria muitos recursos.
- Depender de tipos internos: versões podem mudar detalhes.
- Esquecer emitDestroy: recursos personalizados ficam incompletos.
- Usar baixo nível sem necessidade: AsyncLocalStorage seria mais simples.
Boas práticas para produção
- Use AsyncLocalStorage para contexto comum.
- Ative hooks detalhados apenas quando necessário.
- Aplique amostragem e limites.
- Evite logging assíncrono dentro de callbacks do hook.
- Limpe mapas e recursos personalizados.
- Meça overhead com carga real.
- Documente tipos de AsyncResource próprios.
- Teste contexto em filas e pools.
- Não exponha dados sensíveis.
- Prefira instrumentações maduras em produção.
Conclusão
O módulo Async Hooks no Node.js revela o ciclo de vida dos recursos assíncronos e permite preservar causalidade em abstrações personalizadas. Ele é poderoso para tracing, diagnóstico e desenvolvimento de bibliotecas.
Essa visibilidade possui custo. Use APIs de nível mais alto quando possível, limite dados coletados e teste impacto antes de ativar em produção. Com AsyncResource, limpeza e amostragem, Async Hooks pode solucionar problemas que seriam difíceis de observar apenas com logs.




