Perché non posso buttare dentro un gestore Promise.catch?


127

Perché non posso semplicemente lanciare un Errorcallback interno e lasciare che il processo gestisca l'errore come se fosse in qualsiasi altro ambito?

Se non faccio console.log(err)nulla viene stampato e non so nulla di quello che è successo. Il processo finisce e basta ...

Esempio:

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 i callback vengono eseguiti nel thread principale, perché Errorvengono inghiottiti da un buco nero?


11
Non viene inghiottito da un buco nero. Rifiuta la promessa che .catch(…)ritorna.
Bergi,


invece di .catch((e) => { throw new Error() }), scrivere .catch((e) => { return Promise.reject(new Error()) })o semplicemente.catch((e) => Promise.reject(new Error()))
chharvey

1
@chharvey tutti i frammenti di codice nel tuo commento hanno un comportamento esattamente identico, tranne quello iniziale è ovviamente più chiaro.
Сергей Гринько,

Risposte:


157

Come altri hanno spiegato, il "buco nero" è perché il lancio all'interno di una .catchcatena continua con una promessa respinta, e non hai più catture, portando a una catena non terminata, che ingoia errori (male!)

Aggiungi un altro fermo per vedere cosa sta succedendo:

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!
});

Un fermo nel mezzo di una catena è utile quando si desidera che la catena proceda nonostante un passaggio non riuscito, ma una ripetizione è utile per continuare a fallire dopo aver fatto cose come la registrazione delle informazioni o i passaggi di pulizia, forse anche alterando quale errore viene lanciato.

Trucco

Per far apparire l'errore come un errore nella console web, come avevi inizialmente inteso, uso questo trucco:

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

Persino i numeri di riga sopravvivono, quindi il collegamento nella console Web mi porta direttamente al file e alla riga in cui si è verificato l'errore (originale).

Perché funziona

Qualsiasi eccezione in una funzione chiamata come adempimento della promessa o gestore del rifiuto viene automaticamente convertita in rifiuto della promessa che dovresti restituire. Il codice promettente che chiama la tua funzione si occupa di questo.

Una funzione chiamata da setTimeout, invece, viene sempre eseguita dallo stato stabile JavaScript, ovvero viene eseguita in un nuovo ciclo nel ciclo degli eventi JavaScript. Le eccezioni non vengono rilevate da nulla e arrivano alla console Web. Poiché errcontiene tutte le informazioni sull'errore, inclusi lo stack, il file e il numero di riga originali, viene comunque riportato correttamente.


3
Jib, questo è un trucco interessante, puoi aiutarmi a capire perché funziona?
Brian Keith,

8
A proposito di quel trucco: stai lanciando perché vuoi accedere, quindi perché non semplicemente accedere direttamente? Questo trucco in un momento "casuale" genererà un errore irraggiungibile ... Ma l'idea generale delle eccezioni (e il modo in cui le promesse le gestiscono) è di rendere responsabilità del chiamante catturare l'errore e affrontarlo. Questo codice rende effettivamente impossibile per il chiamante gestire gli errori. Perché non semplicemente creare una funzione per gestirla per te? function logErrors(e){console.error(e)}quindi usalo come do1().then(do2).catch(logErrors). La risposta in se stessa è ottima, +1
Stijn de Witt

3
@jib Sto scrivendo un lambda AWS che contiene molte promesse connesse più o meno come in questo caso. Per sfruttare gli allarmi e le notifiche AWS in caso di errori, devo fare in modo che il crash lambda generi un errore (immagino). Il trucco è l'unico modo per ottenerlo?
Masciugo,

2
@StijndeWitt Nel mio caso, stavo cercando di inviare i dettagli dell'errore al mio server nel window.onerrorgestore eventi. Solo facendo il setTimeouttrucco questo può essere fatto. Altrimenti window.onerrornon sentirò mai nulla degli errori accaduti in Promise.
hudidit,

1
@hudidit Comunque, sia che sia console.logo postErrorToServer, puoi semplicemente fare ciò che deve essere fatto. Non vi è alcun motivo per quale codice sia window.onerrorinserito non può essere inserito in una funzione separata ed essere richiamato da 2 posizioni. Probabilmente è anche più breve della setTimeoutlinea.
Stijn de Witt,

46

Cose importanti da capire qui

  1. Entrambe le funzioni thene catchrestituiscono nuovi oggetti promessa.

  2. Lanciare o rifiutare esplicitamente, sposterà la promessa attuale allo stato rifiutato.

  3. Poiché thene catchrestituiscono nuovi oggetti promessa, possono essere concatenati.

  4. Se lanci o rifiuti all'interno di un gestore di promesse ( theno catch), verrà gestito nel successivo gestore di rifiuto lungo il percorso di concatenamento.

  5. Come indicato da jfriend00, i gestori thene catchnon vengono eseguiti in modo sincrono. Quando un gestore lancia, finirà immediatamente. Quindi, lo stack verrà srotolato e l'eccezione andrebbe persa. Ecco perché lanciare un'eccezione rifiuta l'attuale promessa.


Nel tuo caso, stai rifiutando dentro do1lanciando un Erroroggetto. Ora, la promessa attuale sarà in stato respinto e il controllo verrà trasferito al gestore successivo, che è thennel nostro caso.

Poiché il thengestore non ha un gestore di rifiuto, do2non verrà eseguito affatto. Puoi confermarlo usando console.logal suo interno. Poiché la promessa corrente non ha un gestore di rifiuto, verrà rifiutata anche con il valore di rifiuto della promessa precedente e il controllo verrà trasferito al gestore successivo che è catch.

Come catchè un gestore di rifiuto, quando lo fai console.log(err.stack);al suo interno, sei in grado di vedere la traccia dello stack degli errori. Ora, stai lanciando un Erroroggetto da esso, quindi anche la promessa restituita catchsarà in stato rifiutato.

Poiché non è stato collegato alcun gestore di rifiuto a catch, non è possibile osservare il rifiuto.


Puoi dividere la catena e capirlo meglio, in questo modo

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);
});

L'output che otterrai sarà qualcosa di simile

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

All'interno del catchgestore 1, stai ottenendo il valore promisedell'oggetto come rifiutato.

Allo stesso modo, anche la promessa restituita dal catchgestore 1 viene respinta con lo stesso errore con il quale è promisestata rifiutata e la osserviamo nel secondo catchgestore.


3
Potrebbe anche valere la pena aggiungere che i .then()gestori sono asincroni (lo stack viene srotolato prima che vengano eseguiti), quindi le eccezioni al loro interno devono essere trasformate in rifiuti, altrimenti non ci sarebbero gestori di eccezioni per catturarli.
jfriend00,

7

Ho provato il setTimeout()metodo descritto sopra ...

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

In modo fastidioso, ho trovato questo completamente non verificabile. Poiché sta generando un errore asincrono, non è possibile racchiuderlo in try/catchun'istruzione, poiché l' catchascolto verrà interrotto dal momento in cui viene generato l'errore.

Sono tornato a utilizzare solo un ascoltatore che ha funzionato perfettamente e, poiché è così che JavaScript deve essere usato, era altamente testabile.

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

    /* Throw so the promise is still rejected for testing */
    throw err;
});

3
Jest ha simulazioni temporali che dovrebbero gestire questa situazione.
jordanbtucker,

2

Secondo le specifiche (vedi 3.III.d) :

d. Se chiamare quindi genera un'eccezione e,
  a. Se è stato chiamato resolPromise o rejectPromise, ignorarlo.
  b. Altrimenti, rifiuta la promessa con e come motivo.

Ciò significa che se si lancia un'eccezione in thenfunzione, verrà catturata e la promessa verrà respinta. catchnon ha senso qui, è solo una scorciatoia per.then(null, function() {})

Immagino che tu voglia registrare i rifiuti non gestiti nel tuo codice. La maggior parte delle promesse delle biblioteche fa fuoco unhandledRejectionper questo. Ecco un argomento rilevante con una discussione al riguardo.


Vale la pena ricordare che l' unhandledRejectionhook è per JavaScript lato server, sul lato client diversi browser hanno soluzioni diverse. Non l'abbiamo ancora standardizzato, ma ci sta arrivando lentamente ma sicuramente.
Benjamin Gruenbaum,

1

So che è un po 'tardi, ma mi sono imbattuto in questo thread e nessuna delle soluzioni era facile da implementare per me, quindi ho trovato il mio:

Ho aggiunto una piccola funzione di supporto che restituisce una promessa, in questo modo:

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

Quindi, se ho un posto specifico in una delle mie catene di promesse in cui voglio lanciare un errore (e rifiutare la promessa), torno semplicemente dalla funzione sopra con il mio errore costruito, in questo modo:

}).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);
})

In questo modo sono in grado di lanciare errori extra dall'interno della catena della promessa. Se si desidera gestire anche errori di promessa "normali", si dovrebbe espandere la cattura per trattare separatamente gli errori "auto-generati".

Spero che questo aiuti, è la mia prima risposta stackoverflow!


Promise.reject(error)invece di new Promise(function (resolve, reject){ reject(error) })(che avrebbe comunque bisogno di una dichiarazione di ritorno)
Funkodebat

0

Sì, promette errori di deglutizione e puoi solo colpirli .catch, come spiegato più dettagliatamente in altre risposte. Se ti trovi in ​​Node.js e desideri riprodurre il throwcomportamento normale , stampare lo stack stack sulla console e uscire dal processo, puoi farlo

...
  throw new Error('My error message');
})
.catch(function (err) {
  console.error(err.stack);
  process.exit(0);
});

1
No, non è sufficiente, come dovresti metterlo alla fine di ogni catena di promesse che hai. Piuttosto agganciato unhandledRejectionall'evento
Bergi,

Sì, questo presuppone che tu abbia incatenato le tue promesse, quindi l'uscita è l'ultima funzione e non viene catturata dopo. L'evento di cui parli penso sia solo se si utilizza Bluebird.
Jesús Carrera,

Bluebird, Q, quando, promesse native, ... Probabilmente diventerà uno standard.
Bergi,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.