Pesquisa de site

node.js - Node registrando aviso inesperado UnhandledPromiseRejectionWarning


Eu tenho um pedaço de código que está fazendo o Node registrar UnhandledPromiseRejectionWarning. Mas não tenho certeza do porquê. Aqui está o código resumido:

export class Hello {
  async good(): Promise<string> {
    let errorP = this.throwError();
    let responseP = this.doSomething();
    let [error, response] = await Promise.all([errorP, responseP]);
    return response + '123';
  }

  async bad(): Promise<string> {
    let errorP = this.throwError();
    let responseP = this.doSomething();
    let response = (await responseP) + '123';
    let error = await errorP;
    return response;
  }

  private async throwError(): Promise<string> {
    await (new Promise(resolve => setTimeout(resolve, 1000)));
    throw new Error('error');
  }

  private async doSomething(): Promise<string> {
    await (new Promise(resolve => setTimeout(resolve, 1000)));
    return 'something';
  }
}

Chamar try { await hello.bad(); } catch (err) {} faz com que o node registre UnhandledPromiseRejectionWarning

Chamar try { await hello.good(); } catch (err) {} NÃO registra o aviso

Erro completo:

(node:25960) UnhandledPromiseRejectionWarning: Error: error
    at Hello.<anonymous> (C:\hello-service.ts:19:11)
    at Generator.next (<anonymous>)
    at fulfilled (C:\hello-service.ts:5:58)
    at runNextTicks (internal/process/task_queues.js:58:5)
    at listOnTimeout (internal/timers.js:523:9)
    at processTimers (internal/timers.js:497:7)
(node:25960) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch()
. To terminate the node process on unhandled promise rejection, use the CLI flag `--unhandled-rejections=strict` (see https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). (rejection id: 1)
(node:25960) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
{"level":30,"time":1639745087914,"pid":25960,"hostname":"AZWP10801-12","reqId":"req-1","dd":{"trace_id":"2081604231398834164","span_id":"2081604231398834164","service":"@amerisave/example-service","version":"0.0.0"},"res":{"statu
sCode":200},"responseTime":1025.4359999895096,"msg":"request completed"}
(node:25960) PromiseRejectionHandledWarning: Promise rejection was handled asynchronously (rejection id: 1)

Algumas versões de dependências: node ver. 14.16.1 ts-node-dev ver. 1.1.8 ts-node ver. 9.1.1 typescript ver. 4.5.2

Por que good é bom, mas bad é ruim?

Resposta #1

O problema em bad() ocorre porque a promessa errorP é rejeitada ANTES de você chegar a await errorP e, portanto, ela é rejeitada quando não há um manipulador de rejeição presente. O Nodejs detecta que uma promessa foi rejeitada e seu processo retorna ao loop de eventos, e essa promessa rejeitada não possui um manipulador de rejeição. Isso gera o aviso de "rejeição não tratada".

Observe aqui que, embora await errorP não aplique diretamente um manipulador de rejeição, ele vincula o errorP à função async pai, que possui um manipulador de rejeição, então o await errorP atribui indiretamente o tratamento de rejeição para errorP. Por outro lado, errorP por si só apenas rejeitará e não causará nada na função async pai. Ele será apenas uma variável contendo uma promise agora rejeitada sem manipulador de rejeição.

Para aproveitar a propagação automática de erros de promessas rejeitadas do async, você precisa await essa promessa.

O Nodejs não sabe que você vai adicionar o await no futuro com um código que será executado algum tempo depois, então ele reportará a rejeição não tratada.

O código se torna sujeito a esse tipo de erro quando você coloca uma promessa em uma variável e não tem nenhum manipulador de rejeição nessa promessa de qualquer tipo e então você faz await em alguma outra promessa ANTES de colocar qualquer tipo de manipulador de rejeição nessa promessa anterior. A promessa fica simplesmente lá, sem tratamento de erros. Se, devido ao tempo das coisas, ela for rejeitada nesse estado, você receberá o aviso. As soluções usuais são:

  1. Coloque imediatamente o tratamento de erros na promessa para que ela nunca fique sozinha.
  2. Não crie a promise até você estar pronto para usá-la da maneira que for usá-la, com o tratamento de erro apropriado (com um .then().catch() ou em um Promise.all().catch() ou em um await ou qualquer que seja).
  3. Não await outras promessas enquanto uma promessa estiver guardada em uma variável sem nenhum tratamento de rejeição.

Eu percebo que se eu puder evitar colocar uma promessa sem tratamento em uma variável e, em vez disso, apenas criar a promessa diretamente na circunstância em que ela será monitorada para conclusão e erro, você nem precisa geralmente pensar sobre essa questão.


Para sua informação, você pode ilustrar o mesmo conceito geral de uma promessa sendo rejeitada antes de adicionar um manipulador de rejeição de uma maneira mais simples aqui se você executar isso no nodejs:

function bad(t) {
    return new Promise((resolve, reject) => {
        setTimeout(reject, t);
    });
}

const b = bad(500);

// this timer will fire after bad() rejects
setTimeout(() => {
    b.catch(err => {
        console.log("caught b rejection");
    })
}, 600);

Você receberá o erro "rejeição não capturada" porque, quando a promise é rejeitada, ela ainda não tem um manipulador .catch(). Seu código tem esse mesmo problema (embora um pouco mais disfarçado) porque o manipulador de rejeição vem do await e da função async e do try/catch que o chamador da função async está usando.

Resposta #2

Aqui está uma hipótese (que pode ser provada experimentalmente).

A diferença de comportamento entre good e bad pode ser explicada pela ordem dos awaits.

Em bad você está aguardando no throwError depois de ter aguardado no doSomething, enquanto em good, você está aguardando no Promise.all, que não retornará até que ambos sejam cumpridos ou pelo menos um seja rejeitado (o que será o caso aqui).

Então, em bad, o lançamento está acontecendo fora do await, e seu catch não é acionado, sendo capturado internamente pelo Node.

Se você alterar seu bad para que você await em throwError primeiro, então seu catch será acionado:

async bad(): Promise<string> {
    let errorP = this.throwError();
    let responseP = this.doSomething();
    let error = await errorP;
    let response = (await responseP) + '123';
    return response;
  }

Artigos relacionados