2015-06-08 14 views
89

Perché non posso semplicemente lanciare un Error all'interno della richiamata catch e lasciare che il processo gestisca l'errore come se fosse in qualsiasi altro ambito?Perché non posso lanciare all'interno di un gestore Promise.catch?

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

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 callback vengono eseguiti nel thread principale, il motivo per cui il Error ottiene inghiottito da un buco nero?

+8

Non viene inghiottito da un buco nero. Rifiuta la promessa che '.catch (...)' ritorna. – Bergi

+0

Vedere anche [Perché le eccezioni sono utilizzate per rifiutare le promesse in JS?] (Http://stackoverflow.com/q/21616432/1048572) e [Come gestisco le eccezioni globalmente con promesse native in io.js/node.js? ] (http://stackoverflow.com/q/28709666/1048572) – Bergi

risposta

37

Cose importanti da capire qui

  1. Sia i then e catch funzioni restituiscono nuovi oggetti promessa.

  2. O il lancio o il rifiuto esplicito, sposteranno la promessa corrente allo stato rifiutato.

  3. Poiché then e catch restituiscono nuovi oggetti di promessa, possono essere concatenati.

  4. Se si lancia o si respinge all'interno di un gestore promessa (then o catch), questo verrà gestito nel successivo gestore di rifiuto lungo il percorso di concatenamento.

  5. Come menzionato da jfriend00, i gestori then e catch non vengono eseguiti in modo sincrono. Quando un handler lancia, finirà immediatamente. Quindi, lo stack verrà svolto e l'eccezione verrà persa. Ecco perché lanciare un'eccezione rifiuta l'attuale promessa.


Nel tuo caso, si stanno rifiutando all'interno do1 lanciando un oggetto Error. Ora, la promessa corrente sarà in stato rifiutato e il controllo sarà trasferito al gestore successivo, che è then nel nostro caso.

Poiché il gestore then non ha un gestore di rifiuto, lo do2 non verrà eseguito affatto. È possibile confermare questo utilizzando console.log al suo interno. Poiché la promessa corrente non ha un gestore di rifiuto, sarà anche respinta con il valore di rifiuto dalla precedente promessa e il controllo sarà trasferito al gestore successivo che è catch.

Poiché catch è un gestore di rifiuto, quando si esegue console.log(err.stack); al suo interno, è possibile visualizzare la traccia dello stack degli errori. Ora stai lanciando un oggetto Error così la promessa restituita da catch sarà anch'essa in stato rifiutato.

Poiché non è stato applicato alcun gestore di rifiuto allo catch, non è possibile osservare il rifiuto.


È possibile dividere la catena e capire meglio, come questo

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'uscita si otterrà sarà qualcosa di simile a

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

All'interno del catch gestore 1, si sono ottenendo il valore dell'oggetto promise come rifiutato.

Allo stesso modo, la promessa restituito dal gestore catch 1, è anche respinto con lo stesso errore con cui il promise è stata respinta e ci stanno osservando nel secondo catch gestore.

+3

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

2

Secondo the spec (see 3.III.d):

d. Se la chiamata allora genera un'eccezione e,
    a. Se resolvePromise o rejectPromise sono stati chiamati, ignoralo.
    b. Altrimenti, respingi la promessa con e come ragione.

Ciò significa che se si genera un'eccezione nella funzione then, verrà catturata e la promessa verrà respinta. catch non ha senso qui, è solo una scorciatoia per .then(null, function() {})

Immagino che vogliate registrare i rifiuti non gestiti nel vostro codice. La maggior parte delle biblioteche promesse licenzia un unhandledRejection per questo. Ecco relevant gist con discussione a riguardo.

+0

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

117

Come altri hanno già spiegato, il "buco nero" è perché gettare all'interno di un .catch continua la catena con una promessa respinto, e non hai più catture, che porta a una catena non terminato, che inghiotte gli errori (male!)

aggiungere 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 bel mezzo di una catena è utile quando si desidera la catena di procedere a dispetto di un passaggio non riuscito, ma una ri-tiro è utile continua fallendo dopo aver fatto cose come la registrazione di informazioni o c passi magri, forse anche alterando quale errore viene lanciato.

trucco

a rendere l'errore presentarsi come un errore nella console web, come originariamente previsto, io uso questo trucco:

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

Anche i numeri di riga sopravvivere, così il collegamento in la console Web mi porta direttamente al file e alla riga in cui si è verificato l'errore (originale).

Perché funziona

Ogni eccezione in una funzione chiamata come compimento promessa o gestore di rifiuto ottiene automaticamente convertito in un rifiuto della promessa che si dovrebbe tornare. Il codice di promessa che chiama la tua funzione si occupa di questo.

Una funzione chiamata da setTimeout, d'altra parte, viene sempre eseguita dallo stato stabile di JavaScript, ovvero viene eseguita in un nuovo ciclo nel ciclo di eventi JavaScript. Eccezioni non ci sono catturati da nulla, e lo fanno alla console web. Poiché err contiene tutte le informazioni sull'errore, inclusi lo stack originale, il file e il numero di riga, viene comunque segnalato correttamente.

+2

Jib, questo è un trucco interessante, puoi aiutarmi a capire perché funziona? –

+0

@BrianKeith che funziona perché 'setTimeout' è un timer. Poiché i timer accodano l'esecuzione delle loro richiamate a un momento specifico nel futuro, non fanno parte dello stack di chiamate della funzione immediata. Per ulteriori informazioni sulla relazione tra lo stack di chiamate e la coda di eventi, ci sono alcune letture solide e diagrammi sulla Mozilla Developer Network qui: https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop – James

+0

Con la stessa logica, quando si programma in node.js, usando 'process.nextTick' al posto di' setTimeout' si eseguirà la stessa azione. Ora ho un buon metodo che posso lanciare alla fine di una fastidiosa catena Promise per uccidere il programma se rovino la sintassi da qualche parte. – Tustin2121

0

Sì promette di ingoiare gli errori e li puoi catturare solo con .catch, come spiegato più dettagliatamente in altre risposte. Se siete in Node.js e si desidera riprodurre il normale throw comportamento, tracce stack di stampa per consolare e processo di uscita, si può fare

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

No, non è sufficiente, in quanto dovresti metterlo alla fine di * ogni * catena di promesse che hai. Piuttosto [agganciare all'evento 'unhandledRejection'] (http://stackoverflow.com/a/28004999/1048572) – Bergi

+0

Sì, supponendo che tu leghi le tue promesse in modo che l'uscita sia l'ultima funzione e che non venga catturata dopo. L'evento che hai citato penso che sia solo se utilizzi Bluebird. –

+0

Bluebird, Q, quando, promesse native, ... Probabilmente diventerà uno standard. – Bergi

3

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

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

Annoyingly, ho trovato questo completamente non testabile. Poiché genera un errore asincrono, non è possibile racchiuderlo all'interno di un'istruzione try/catch, poiché lo catch avrà interrotto l'ascolto per l'ora in cui viene generato l'errore.

Sono tornato a utilizzare solo un listener che ha funzionato perfettamente e, poiché è come si intende utilizzare JavaScript, era altamente verificabile.

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

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

Jest ha [timer mock] (https://facebook.github.io/jest/docs/timer-mocks.html) che dovrebbe gestire questa situazione. – jordanbtucker