Promesses cassées ?
Des erreurs ignorées, des résultats perdus...
Les promesses JavaScript sont-elles cassées ?
Au temps d’avant
L’un des mythes les plus courants sur les Promesses concerne leurs prétendues lacunes en matière de gestion des erreurs.
Il y a de nombreuses années, les promesses étaient effectivement catastrophiques avec les erreurs. Beaucoup de travail a été consacré à corriger cela.
Et voici, c’est corrigé, et même largement déployé.
Les gens ont exulté
Et malheureusement, certains ne l’ont pas remarqué.
À l’heure actuelle
Le mythe persiste, je le vois partout : articles populaires sur Medium, sur DZone, et beaucoup d’autres sources.
J’avoue que même les ressources et la documentation « officielles » proposent surtout des exemples fragiles et de mauvaises habitudes. Ils sont souvent utilisés pour « prouver » le procès fait aux Promesses. Certains suggèrent même des « remèdes » qui aggravent considérablement les choses. (note : lien supprimé)
Les règles pour éviter les ennuis
- Les promesses ont besoin de quelque chose auquel se raccrocher
- Toujours
return(retourner une valeur) dans vos fonctions.
- Toujours
- Utilisez de véritables instances
Error- Toujours utiliser des instances
Error.
- Toujours utiliser des instances
- Gérez les erreurs là où cela a du sens
- Toujours utiliser
.catch(), au moins une fois.
- Toujours utiliser
- Apportez de la clarté avec des fonctions nommées 🦄✨
- Préférez les fonctions nommées.
#1 Les promesses ont besoin de quelque chose auquel se raccrocher
Il est essentiel de toujours return dans vos fonctions.
Les fonctions de rappel des promesses suivent un certain modèle dans .then(callback) et .catch(callback).
Chaque valeur retournée est transmise au rappel du .then() suivant.
function addTen(number) { return number + 10;}
Promise.resolve(10) // 10 .then(addTen) // 20 .then(addTen) // 30 .then(addTen) // 40 .then(console.log) // logs "40"Bonus du « toujours retourner » : le code est beaucoup plus facile à tester unitairement.
Question : Combien d’états distincts de Promesse (résolue et rejetée) ont été créés ?
Question : Combien de promesses ont été créées dans l’exemple précédent ?
#2 Utilisez de véritables instances Error
JavaScript a un comportement intéressant autour des erreurs (qui s’applique au code asynchrone et synchrone).
[voir l’exemple sur repl.it :throwing errors in javascript]
Pour obtenir des détails utiles sur le numéro de ligne et la pile d’appels, vous devez utiliser des instances Error. Lancer des chaînes ne fonctionne pas comme en Python ou Ruby.
Bien que JavaScript semble gérer throw "string", car vous verrez la chaîne dans votre gestionnaire catch. Cependant, les données sont tout ce que vous verrez*. Aucun stack frame précédent ne sera inclus.
Exemples corrects avec new Error :
throw new Error('message') // ✅Promise.reject(new Error('message')) // ✅throw Error('message') // ✅Promise.reject(Error('message')) // ✅Voici des anti-modèles courants :
throw 'error message' // ❌Promise.reject(-42) // ❌#3 Gérez les erreurs là où cela a du sens
Les promesses offrent une méthode élégante pour gérer les erreurs, en utilisant .catch(). C’est essentiellement un type spécial de .then() - où toutes les erreurs des .then() précédents sont gérées. Regardons un exemple…
Promise.resolve(42) .then(() => 'hello') .catch(() => console.log('will not get hit')) .then(() => throw new Error('totes fail')) .catch(() => console.log('WILL get hit'))Bien que .catch() puisse ressembler à un gestionnaire d’événements DOM (ex. click, keypress). Son emplacement est important, car il ne peut « intercepter » que les erreurs lancées au-dessus de lui.
Surcharger les erreurs est relativement trivial Retournez une valeur non-erreur dans votre rappel .catch(), et la chaîne de promesses bascule vers l’exécution des rappels .then() en séquence. (En pratique.)
Essayez de suivre la séquence de l’exemple suivant :
Promise.resolve(42) .then(() => 'hello') .then(() => throw new Error('totes fail')) .catch(() => { return 99 }) .then(num => num + 1) .then(console.log) // expected output: 100C’est la séquence qu’il est important de comprendre.
Bien que cet exemple soit simpliste, il est conçu pour illustrer comment les erreurs et les données circulent dans les promesses.
Voici un aperçu de la séquence :
- 42 est la valeur initiale.
helloest toujours retourné par la méthode suivante.- nous ignorons la valeur précédente, et lançons une erreur avec le message
'totes fail'. .catch()intercepte l’erreur, retourne99à la place, qui sera géré par tout.then()suivant.- incrémente
num, retournant100 - la méthode
console.logreçoit100et l’affiche ! :tada:
Question : Que se passe-t-il lorsque 2 .catch() se suivent ? Le second peut-il s’exécuter ? Pouvez-vous penser à un cas d’utilisation ?
Question : Comment .catch() peut-il ignorer des erreurs ? Comment empêcheriez-vous les erreurs de forcer une sortie prématurée de Promise.all ?
#4 Apportez de la clarté avec des fonctions nommées 🦄✨
Comparez la lisibilité des 2 exemples suivants :
Anonyme : ❌
Promise.resolve(10) // 10 .then(x => x * 2) // 20 .then(x => x / 4) // 5 .then(x => x * x) // 25 .then(x => x.toFixed(2)) // "25.00" .then(x => console.log(x)) // expected output: "25.00"Nommé : ✅
Promise.resolve(10) // 10 .then(double) // 20 .then(quarter) // 5 .then(square) // 25 .then(format) // "25.00" .then(log) // expected output: "25.00"
const double = x => x * 2const quarter = x => x / 4const square = x => x * xconst format = x => x.toFixed(2)const log = x => console.log(x)BONUS : ✅
Compatible avec les méthodes de tableau !!!
Vous pouvez réutiliser vos fonctions nommées avec nos amies de Array.prototype. Notamment .map(), .filter(), .every(), .some(), .find() !
Les pipelines de collection #FTW :
// IT'S LIKE THE SAME THING :mindblown:
[10, 20] // [ 10, 20 ] .map(double) // [ 20, 40 ] .map(quarter) // [ 5, 10 ] .map(square) // [ 25, 100 ] .map(format) // [ "25.00", "100.00" ] .map(log) // expected 2 lines of output: "25.00", "100.00"Et si vous ne voulez pas faire ce codage linéaire… Eh bien, vous avez des fonctions simples !
Vous pouvez les utiliser comme bon vous semble :
// Nesting patern// ❌ please don't do this, however
const result = format(square(quarter(double(10))))
log(result)// expected output: "25.00"Pourquoi l’imbrication de fonctions est-elle un anti-modèle ?
- Pas aussi lisible pour autant de personnes
- les diffs git ne révèlent pas facilement qui a changé quoi
- difficile à déboguer ou à journaliser depuis le milieu des fonctions imbriquées