Home > Blog > Desenvolvimento Web
Desenvolvimento Web
JavaScript
Programação

Async Hooks no Node.js: Guia Prático

Atualizado em: 9 de agosto de 2026

Estação de trabalho usada no desenvolvimento de aplicações Node.js

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.

Os 10 Melhores Cursos de Programação de 2026

Descubra os melhores cursos de programação. Aprenda a escolher o curso ideal para iniciar ou avançar na carreira de desenvolvedor

POSTS RELACIONADOS

Ver todos

Seta para a direita