Pourquoi ne puis-je pas lancer un gestionnaire Promise.catch?


127

Pourquoi ne puis-je pas simplement lancer un Errorrappel à l'intérieur du catch et laisser le processus gérer l'erreur comme si elle était dans une autre portée?

Si je ne fais console.log(err)rien, rien n'est imprimé et je ne sais rien de ce qui s'est passé. Le processus se termine juste ...

Exemple:

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

Si les rappels sont exécutés dans le thread principal, pourquoi Errorest-il avalé par un trou noir?


11
Il n'est pas avalé par un trou noir. Il rejette la promesse qui .catch(…)revient.
Bergi


au lieu de .catch((e) => { throw new Error() }), écrivez .catch((e) => { return Promise.reject(new Error()) })ou simplement.catch((e) => Promise.reject(new Error()))
chharvey

1
@chharvey tous les extraits de code de votre commentaire ont un comportement exactement identique, sauf que celui initial est évidemment le plus clair.
Сергей Гринько

Réponses:


157

Comme d'autres l'ont expliqué, le "trou noir" est parce que jeter à l'intérieur d'un .catchcontinue la chaîne avec une promesse rejetée, et vous n'avez plus de prises, conduisant à une chaîne inachevée, qui avale les erreurs (mauvais!)

Ajoutez une autre capture pour voir ce qui se passe:

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

Une prise au milieu d'une chaîne est utile lorsque vous voulez que la chaîne continue malgré une étape échouée, mais une relance est utile pour continuer à échouer après avoir fait des choses comme la journalisation d'informations ou les étapes de nettoyage, peut-être même en modifiant l'erreur. Est lancé.

Tour

Pour que l'erreur s'affiche comme une erreur dans la console Web, comme vous l'aviez initialement prévu, j'utilise cette astuce:

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

Même les numéros de ligne survivent, donc le lien dans la console Web m'amène directement au fichier et à la ligne où l'erreur (d'origine) s'est produite.

Pourquoi ça marche

Toute exception dans une fonction appelée comme gestionnaire de traitement de promesse ou de rejet est automatiquement convertie en un rejet de la promesse que vous êtes censé renvoyer. Le code de promesse qui appelle votre fonction s'en charge.

Une fonction appelée par setTimeout en revanche, fonctionne toujours à partir de l'état stable JavaScript, c'est-à-dire qu'elle s'exécute dans un nouveau cycle dans la boucle d'événements JavaScript. Les exceptions là-bas ne sont détectées par rien et parviennent à la console Web. Puisque errcontient toutes les informations sur l'erreur, y compris la pile d'origine, le fichier et le numéro de ligne, il est toujours signalé correctement.


3
Jib, c'est une astuce intéressante, pouvez-vous m'aider à comprendre pourquoi cela fonctionne?
Brian Keith du

8
À propos de cette astuce: vous lancez parce que vous voulez vous connecter, alors pourquoi ne pas vous connecter directement? Cette astuce jettera à un moment «aléatoire» une erreur incontrôlable ... Mais toute l'idée des exceptions (et la façon dont les promesses les traitent) est de faire de l' appelant la responsabilité de détecter l'erreur et de la traiter. Ce code rend effectivement impossible pour l'appelant de traiter les erreurs. Pourquoi ne pas simplement créer une fonction pour la gérer pour vous? function logErrors(e){console.error(e)}puis utilisez-le comme do1().then(do2).catch(logErrors). La réponse elle-même est géniale btw, +1
Stijn de Witt

3
@jib J'écris un AWS lambda qui contient de nombreuses promesses connectées plus ou moins comme dans ce cas. Pour exploiter les alarmes et notifications AWS en cas d'erreurs, je dois faire en sorte que le crash lambda génère une erreur (je suppose). L'astuce est-elle le seul moyen d'y parvenir?
masciugo du

2
@StijndeWitt Dans mon cas, j'essayais d'envoyer les détails de l'erreur à mon serveur dans le window.onerrorgestionnaire d'événements. Ce n'est qu'en faisant le setTimeouttour que cela peut être fait. Sinon window.onerror, vous n'entendrez jamais rien des erreurs survenues dans Promise.
hudidit

1
@hudidit Pourtant, que ce soit console.logou postErrorToServer, vous pouvez simplement faire ce qui doit être fait. Il n'y a aucune raison pour laquelle le code qui se trouve window.onerrorne peut pas être pris en compte dans une fonction distincte et être appelé à partir de 2 endroits. C'est probablement encore plus court que la setTimeoutligne.
Stijn de Witt

46

Choses importantes à comprendre ici

  1. Les fonctions thenet catchrenvoient de nouveaux objets de promesse.

  2. Le fait de lancer ou de rejeter explicitement fera passer la promesse actuelle à l'état rejeté.

  3. Depuis thenet catchrenvoyer de nouveaux objets de promesse, ils peuvent être enchaînés.

  4. Si vous lancez ou rejetez dans un gestionnaire de promesses ( thenou catch), il sera traité dans le prochain gestionnaire de rejet sur le chemin de chaînage.

  5. Comme mentionné par jfriend00, les gestionnaires thenet catchne sont pas exécutés de manière synchrone. Quand un handler lance, cela prendra fin immédiatement. Ainsi, la pile sera déroulée et l'exception serait perdue. C'est pourquoi lancer une exception rejette la promesse actuelle.


Dans votre cas, vous rejetez à l'intérieur do1en lançant un Errorobjet. Désormais, la promesse actuelle sera rejetée et le contrôle sera transféré au gestionnaire suivant, ce qui est thendans notre cas.

Puisque le thengestionnaire n'a pas de gestionnaire de rejet, le do2ne sera pas exécuté du tout. Vous pouvez le confirmer en utilisant à l' console.logintérieur. Étant donné que la promesse actuelle n'a pas de gestionnaire de rejet, elle sera également rejetée avec la valeur de rejet de la promesse précédente et le contrôle sera transféré au gestionnaire suivant qui est catch.

Comme catchc'est un gestionnaire de rejet, lorsque vous le faites à l' console.log(err.stack);intérieur, vous pouvez voir la trace de la pile d'erreurs. Maintenant, vous lancez un Errorobjet à partir de celui-ci, donc la promesse renvoyée par catchsera également dans un état rejeté.

Puisque vous n'avez attaché aucun gestionnaire de rejet au catch, vous ne pouvez pas observer le rejet.


Vous pouvez diviser la chaîne et mieux comprendre cela, comme ça

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

La sortie que vous obtiendrez sera quelque chose comme

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

Dans le catchgestionnaire 1, vous obtenez la valeur de l' promiseobjet comme rejeté.

De la même manière, la promesse retournée par le catchgestionnaire 1 est également rejetée avec la même erreur avec laquelle le a promiseété rejeté et nous l'observons dans le second catchgestionnaire.


3
Cela pourrait également valoir la peine d'ajouter que les .then()gestionnaires sont asynchrones (la pile est déroulée avant d'être exécutés), donc les exceptions à l'intérieur doivent être transformées en rejets, sinon il n'y aurait pas de gestionnaires d'exceptions pour les attraper.
jfriend00

7

J'ai essayé la setTimeout()méthode détaillée ci-dessus ...

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

Malheureusement, j'ai trouvé cela complètement impossible à tester. Dans la mesure où il génère une erreur asynchrone, vous ne pouvez pas l'envelopper dans une try/catchinstruction, car catchil aura cessé d'écouter au moment où l'erreur est générée.

Je suis revenu à utiliser simplement un auditeur qui fonctionnait parfaitement et, parce que c'est ainsi que JavaScript est censé être utilisé, était hautement testable.

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 a des simulations de minuterie qui devraient gérer cette situation.
jordanbtucker

2

Selon la spécification (voir 3.III.d) :

ré. Si l'appel lève alors une exception e,
  a. Si résoudrePromise ou RejeterPromise ont été appelés, ignorez-le.
  b. Sinon, rejetez la promesse avec e comme raison.

Cela signifie que si vous lancez une exception dans la thenfonction, elle sera interceptée et votre promesse sera rejetée. catchça n'a pas de sens ici, c'est juste un raccourci vers.then(null, function() {})

Je suppose que vous voulez enregistrer les rejets non gérés dans votre code. La plupart des bibliothèques promettent un unhandledRejectionpour cela. Voici l' essentiel pertinent avec une discussion à ce sujet.


Il convient de mentionner que le unhandledRejectionhook est pour JavaScript côté serveur, du côté client, différents navigateurs ont des solutions différentes. NOUS ne l'avons pas encore normalisé mais il y arrive lentement mais sûrement.
Benjamin Gruenbaum

1

Je sais que c'est un peu tard, mais je suis tombé sur ce fil, et aucune des solutions n'était facile à mettre en œuvre pour moi, alors j'ai proposé la mienne:

J'ai ajouté une petite fonction d'aide qui renvoie une promesse, comme ceci:

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

Ensuite, si j'ai une place spécifique dans l'une de mes chaînes de promesses où je veux lancer une erreur (et rejeter la promesse), je reviens simplement de la fonction ci-dessus avec mon erreur construite, comme ceci:

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

De cette façon, je suis en contrôle de lancer des erreurs supplémentaires de l'intérieur de la chaîne de promesses. Si vous souhaitez également gérer les erreurs de promesse «normales», vous étendrez votre capture pour traiter séparément les erreurs «auto-lancées».

J'espère que cela aide, c'est ma première réponse stackoverflow!


Promise.reject(error)au lieu de new Promise(function (resolve, reject){ reject(error) })(qui aurait besoin d'une déclaration de retour de toute façon)
Funkodebat

0

Oui promet d'avaler des erreurs, et vous ne pouvez les attraper qu'avec .catch, comme expliqué plus en détail dans d'autres réponses. Si vous êtes dans Node.js et que vous souhaitez reproduire le throwcomportement normal , en imprimant la trace de la pile sur la console et en quittant le processus, vous pouvez faire

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

1
Non, ce n'est pas suffisant, car vous auriez besoin de mettre cela à la fin de chaque chaîne de promesses que vous avez. Plutôt accro à l' unhandledRejectionévénement
Bergi

Oui, cela suppose que vous enchaînez vos promesses afin que la sortie soit la dernière fonction et qu'elle ne soit pas rattrapée après. L'événement que vous mentionnez, je pense que ce n'est que si vous utilisez Bluebird.
Jesús Carrera

Bluebird, Q, quand, les promesses natives,… Cela deviendra probablement un standard.
Bergi
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.