1

Pitch: O wrap async no Express resolve metade do problema

Eu fiz o Maestro pra um problema chato do Express 4. Você escreve async, dá throw ou a promise rejeita, e o next(err) nunca é chamado. A request trava ou o processo cai.

É o contrato antigo do Express: handler síncrono chama next(err). Promise rejeitada não entra nesse caminho.

No Express 5 isso já é nativo. Wrapper vira opcional.

Mesmo assim eu voltei o pacote. Não pra virar clone do express-async-handler. O wrap é commodity. O que eu queria no meu código era a outra metade: erro de domínio com tipo, sem um app.use gigante de instanceof.

Pacote: maestro-express-async-errors. Zero dependência de runtime. Node 18+.

A metade técnica

maestro(fn) envolve a rota e manda rejeição pro middleware de erro. Tem alias asyncHandler. Quem já usa o pacote clássico troca o import e segue.

import { maestro } from "maestro-express-async-errors";

app.get("/users/:id", maestro(async (req, res) => {
  const user = await repository.getById(req.params.id);
  if (!user) throw new UserNotFoundError(req.params.id);
  res.json(user);
}));

Se você quer o estilo "um import e pronto" do express-async-errors, tem import "maestro-express-async-errors/register". Só mexe no Express 4. No 5 eu deixo no-op de propósito: o framework já captura a promise, e patch em cima arrisca next(err) duas vezes. Depois de subir pro 5, apaga o register.

A outra metade: o erro é contrato

A maioria dos wrappers para aqui. O erro chega no middleware. O que sobra é um catch-all com instanceof empilhado: status, mensagem e log no mesmo bloco. Cada erro novo inchando o mesmo if.

maestro.from quebra isso em middlewares pequenos. A rota só dá throw. O mapeamento fica ao lado da classe.

class UserNotFoundError extends Error {
  readonly code = "USER_NOT_FOUND" as const;
  readonly statusCode = 404;

  constructor(readonly userId: string) {
    super(`User ${userId} not found`);
    this.name = "UserNotFoundError";
  }
}

app.use(
  maestro.from(UserNotFoundError, (err, req, res, next) => {
    res.status(err.statusCode).json({
      code: err.code,
      userId: err.userId,
      message: err.message,
    });
  })
);

app.use((err, req, res, next) => {
  res.status(500).json({ error: "internal" });
});

O from estreita o err pra classe que você passou. userId, code, statusCode sem cast. O Maestro não embarca hierarquia HTTP. Isso fica no app.

Erro que não bate cai no próximo middleware. Você empilha um from por tipo e deixa o genérico por último. Erro deixa de ser string solta e vira contrato entre camadas.

Em app nova no Express 5 o wrap async pode sobrar. O from ainda vale se você quiser esse roteamento sem reinventar.

Quando eu usaria (e quando não)

Usaria em app ainda no Express 4, ou em time que quer erro de domínio tipado sem montar um mini-framework.

Não usaria em app nova no Express 5 sem necessidade do from. Fica no runtime nativo. Só patch, sem from, e o express-async-errors já resolve: sem troca por troca.

O wrap per-rota é a mesma ideia do express-async-handler. O patch no 4 é a mesma DX do express-async-errors. Eu juntei os dois no mesmo pacote, com o patch opt-in e o from por cima. A razão pra olhar o Maestro é TypeScript + domínio no mesmo lugar.

Maestro leva erro async até o middleware certo. O from trata domínio como contrato. Patch global só no Express 4.

Repo: jjgouveia/maestro-express-async-errors

Carregando publicação patrocinada...