Por que não consigo jogar dentro de um manipulador Promise.catch?

127

Por que não consigo simplesmente inserir um Errorretorno de chamada de captura e permitir que o processo lide com o erro como se estivesse em outro escopo?

Se não o fizer, console.log(err)nada será impresso e não sei nada sobre o que aconteceu. O processo acaba ...

Exemplo:

function do1() {
    return new Promise(function(resolve, reject) {
        throw new Error('do1');
        setTimeout(resolve, 1000)
    });
}

function do2() {
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            reject(new Error('do2'));
        }, 1000)
    });
}

do1().then(do2).catch(function(err) {
    //console.log(err.stack); // This is the only way to see the stack
    throw err; // This does nothing
});

Se os retornos de chamada são executados no thread principal, por que eles Errorsão engolidos por um buraco negro?

demian85
fonte
11
Não é engolido por um buraco negro. Rejeita a promessa de que.catch(…) retorna.
Bergi
ao invés de .catch((e) => { throw new Error() }) , escreva .catch((e) => { return Promise.reject(new Error()) })ou simplesmente.catch((e) => Promise.reject(new Error()))
chharvey
1
@chharvey Todos os trechos de código no seu comentário têm um comportamento exatamente idêntico, exceto que o inicial é obviamente mais claro.
Сергей Гринько

Respostas:

157

Como outros explicaram, o "buraco negro" é porque jogar dentro de um .catchcontinua a corrente com uma promessa rejeitada, e você não tem mais capturas, levando a uma corrente não terminada, que engole erros (ruim!)

Adicione mais uma captura para ver o que está acontecendo:

do1().then(do2).catch(function(err) {
    //console.log(err.stack); // This is the only way to see the stack
    throw err; // Where does this go?
}).catch(function(err) {
    console.log(err.stack); // It goes here!
});

Uma captura no meio de uma cadeia é útil quando você deseja que a cadeia continue apesar de uma etapa com falha, mas um re-lançamento é útil para continuar falhando depois de fazer coisas como registro de informações ou etapas de limpeza, talvez até alterando qual erro é jogado.

Truque

Para fazer com que o erro apareça como um erro no console da web, como você pretendia originalmente, use este truque:

.catch(function(err) { setTimeout(function() { throw err; }); });

Até os números das linhas sobrevivem, então o link no console da web me leva diretamente ao arquivo e à linha onde ocorreu o erro (original).

Por que funciona

Qualquer exceção em uma função chamada como manipulador de cumprimento ou rejeição de promessa é convertida automaticamente em uma rejeição da promessa que você deve retornar. O código da promessa que chama sua função cuida disso.

Uma função chamada por setTimeout, por outro lado, sempre é executada no estado estável do JavaScript, ou seja, é executada em um novo ciclo no loop de eventos do JavaScript. Exceções não são detectadas por nada e chegam ao console da web. Como errcontém todas as informações sobre o erro, incluindo a pilha original, o arquivo e o número da linha, ele ainda é relatado corretamente.

jib
fonte
3
Jib, esse é um truque interessante, você pode me ajudar a entender por que isso funciona?
9789 Brian O'Neill,
8
Sobre esse truque: você está jogando porque deseja fazer logon, por que não fazer logon diretamente? Esse truque, em um momento 'aleatório', gera um erro inatacável ... Mas toda a idéia de exceções (e a maneira como as promessas lidam com elas) é tornar responsabilidade do chamador capturar o erro e lidar com ele. Esse código efetivamente impossibilita o chamador de lidar com os erros. Por que não criar uma função para lidar com isso para você? function logErrors(e){console.error(e)}então use-o como do1().then(do2).catch(logErrors). A resposta em si é ótima, +1
Stijn de Witt
3
Estou escrevendo uma AWS lambda que contém muitas promessas relacionadas mais ou menos como neste caso. Para explorar os alarmes e as notificações da AWS em caso de erros, preciso fazer com que a falha do lambda gere um erro (eu acho). O truque é a única maneira de obter isso?
masciugo
2
@StijndeWitt No meu caso, eu estava tentando enviar detalhes do erro ao meu servidor no window.onerrormanipulador de eventos. Somente fazendo o setTimeouttruque isso pode ser feito. Caso contrário window.onerror, nunca ouvirá nada sobre os erros ocorridos no Promise.
Hudidit
1
@ hudidit Ainda assim, seja console.logou não postErrorToServer, você pode simplesmente fazer o que precisa ser feito. Não há razão para que o código esteja window.onerrorinserido não pode ser fatorado em uma função separada e ser chamado de 2 lugares. Provavelmente é ainda mais curto que a setTimeoutlinha.
Stijn de Witt
46

Coisas importantes a entender aqui

  1. As funções thene catchretornam novos objetos de promessa.

  2. Lançar ou rejeitar explicitamente, moverá a promessa atual para o estado rejeitado.

  3. Como thene catchretornar novos objetos de promessa, eles podem ser encadeados.

  4. Se você lançar ou rejeitar dentro de um manipulador de promessa ( thenou catch), ele será tratado no próximo manipulador de rejeição no caminho de encadeamento.

  5. Conforme mencionado por jfriend00, os manipuladores thene catchnão são executados de forma síncrona. Quando um manipulador lança, ele termina imediatamente. Portanto, a pilha será desenrolada e a exceção será perdida. É por isso que lançar uma exceção rejeita a promessa atual.


No seu caso, você está rejeitando do1por dentro jogando um Errorobjeto. Agora, a promessa atual estará no estado rejeitado e o controle será transferido para o próximo manipulador, que é o thennosso caso.

Como o thenmanipulador não possui um manipulador de rejeição, do2ele não será executado. Você pode confirmar isso usando console.logdentro dele. Como a promessa atual não possui um manipulador de rejeição, ela também será rejeitada com o valor de rejeição da promessa anterior e o controle será transferido para o próximo manipulador que é catch.

Como catché um manipulador de rejeição, quando você faz console.log(err.stack);dentro dele, é possível ver o rastreamento da pilha de erros. Agora, você está jogando um Errorobjeto para que a promessa retornada catchtambém esteja no estado rejeitado.

Como você não anexou nenhum manipulador de rejeição ao catch, não é possível observar a rejeição.


Você pode dividir a cadeia e entender isso melhor, assim

var promise = do1().then(do2);

var promise1 = promise.catch(function (err) {
    console.log("Promise", promise);
    throw err;
});

promise1.catch(function (err) {
    console.log("Promise1", promise1);
});

A saída que você obterá será algo como

Promise Promise { <rejected> [Error: do1] }
Promise1 Promise { <rejected> [Error: do1] }

Dentro do catchmanipulador 1, você está recebendo o valor do promiseobjeto como rejeitado.

Da mesma forma, a promessa retornada pelo catchmanipulador 1 também é rejeitada com o mesmo erro com o qual o promisefoi rejeitado e estamos observando-o no segundo catchmanipulador.

thefourtheye
fonte
3
Também vale a pena acrescentar que os .then()manipuladores são assíncronos (a pilha é desenrolada antes de serem executados); portanto, as exceções dentro deles devem ser transformadas em rejeições, caso contrário não haveria manipuladores de exceção para capturá-los.
precisa saber é o seguinte
7

Eu tentei o setTimeout()método detalhado acima ...

.catch(function(err) { setTimeout(function() { throw err; }); });

Irritantemente, achei que isso era completamente não testável. Como está lançando um erro assíncrono, você não pode envolvê-lo em uma try/catchinstrução, porque o catchitem parou de escutar quando o erro é lançado.

Voltei a usar apenas um ouvinte que funcionasse perfeitamente e, porque é como o JavaScript deve ser usado, era altamente testável.

return new Promise((resolve, reject) => {
    reject("err");
}).catch(err => {
    this.emit("uncaughtException", err);

    /* Throw so the promise is still rejected for testing */
    throw err;
});
RiggerTheGeek
fonte
3
Jest tem zombarias de timer que devem lidar com essa situação.
Jordanbtucker 17/08
2

De acordo com as especificações (ver 3.III.d) :

d. Se a chamada então lança uma exceção e,
  a. Se resolvePromise ou rejectPromise tiver sido chamado, ignore-o.
  b. Caso contrário, rejeite a promessa com e como o motivo.

Isso significa que, se você der uma exceção na thenfunção, ela será capturada e sua promessa será rejeitada. catchnão faz sentido aqui, é apenas um atalho para.then(null, function() {})

Acho que você deseja registrar rejeições não tratadas no seu código. A maioria das bibliotecas de promessas dispara unhandledRejectionpor isso. Aqui está a essência da discussão sobre o assunto.

just-boris
fonte
Vale ressaltar que o unhandledRejectiongancho é para JavaScript no servidor, no lado do cliente diferentes navegadores têm soluções diferentes. Ainda não o padronizamos, mas está chegando lá lenta mas seguramente.
Benjamin Gruenbaum
1

Eu sei que isso é um pouco tarde, mas me deparei com esse segmento, e nenhuma das soluções era fácil de implementar para mim, então criei as minhas:

Eu adicionei uma pequena função auxiliar que retorna uma promessa, assim:

function throw_promise_error (error) {
 return new Promise(function (resolve, reject){
  reject(error)
 })
}

Então, se eu tiver um lugar específico em qualquer uma das minhas cadeias de promessas em que desejo gerar um erro (e rejeitar a promessa), simplesmente retornarei da função acima com meu erro construído, da seguinte forma:

}).then(function (input) {
 if (input === null) {
  let err = {code: 400, reason: 'input provided is null'}
  return throw_promise_error(err)
 } else {
  return noterrorpromise...
 }
}).then(...).catch(function (error) {
 res.status(error.code).send(error.reason);
})

Dessa forma, estou no controle de lançar erros extras de dentro da cadeia de promessas. Se você também deseja lidar com erros de promessa 'normais', expanda sua captura para tratar os erros 'lançados automaticamente' separadamente.

Espero que isso ajude, é a minha primeira resposta de stackoverflow!

nuudles
fonte
Promise.reject(error)em vez de new Promise(function (resolve, reject){ reject(error) })(o que seria necessário um qualquer maneira instrução de retorno)
Funkodebat
0

Sim promete erros de andorinha, e você só pode pegá-los .catch, como explicado em detalhes em outras respostas. Se você estiver no Node.js e desejar reproduzir o throwcomportamento normal , imprimindo o rastreamento da pilha para o console e sair do processo, poderá fazer

...
  throw new Error('My error message');
})
.catch(function (err) {
  console.error(err.stack);
  process.exit(0);
});
Jesús Carrera
fonte
1
Não, isso não é suficiente, pois você precisaria colocar isso no final de toda cadeia de promessas que tiver. Em vez disso, gancho no unhandledRejectionevento
Bergi 7/11
Sim, supondo que você encadeie suas promessas para que a saída seja a última função e não seja apanhada depois. O evento que você mencionou, acho que é apenas se estiver usando o Bluebird.
Jesús Carrera
Bluebird, Q, quando, promessas nativas, ... Provavelmente se tornará um padrão.
Bergi