DanLevy.net

Promesses non tenues ?

Ignorer les erreurs, perdre les résultats...

Hero image for Promesses non tenues ?

Les Promises JavaScript sont‑elles cassées ?

À l’époque d’avant

L’un des mythes les plus répandus sur les Promises concerne leurs prétendues lacunes en matière d’erreurs.

Il y a de nombreuses années, les Promises étaient réellement mauvaises avec les erreurs. Beaucoup d’efforts ont été investis pour les corriger.

Et voilà, c’est résolu, même déployé à grande échelle.

La foule a célébré

Et malheureusement, certains ne l’ont pas remarqué.

L’époque actuelle

Le mythe persiste encore, je le vois partout : articles populaires sur Medium, sur DZone, et de nombreuses autres sources.

J’avoue, même les ressources « officielles » et la documentation proposent surtout des exemples fragiles et de mauvaises habitudes. Ils sont souvent utilisés pour « prouver » le cas contre les Promises. Certains suggèrent même des « remèdes » qui aggravent considérablement les choses. (note : lien retiré)



Règles pour éviter les problèmes

  1. Les promesses ont besoin de quelque chose à retenir
    • Toujours return depuis vos fonctions.
  2. Utilisez de vraies instances Error
    • Toujours utiliser des instances Error.
  3. Gérez les erreurs là où cela a du sens
    • Toujours utiliser .catch(), au moins une fois.
  4. Apportez de la clarté avec des fonctions nommées 🦄✨
    • Préférez les fonctions nommées.

#1 Les promesses ont besoin de quelque chose à retenir

Il est crucial que vous toujours return depuis vos fonctions.

Les fonctions de rappel des promesses suivent un certain schéma dans .then(callback) et .catch(callback).

Chaquevaleur retournée est transmise au callback 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"

Le bonus du « toujours retourner » : le code devient beaucoup plus facile à tester unitairement.

Question : Combien d’états distincts de Promise (résolus & rejetés) 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 possède 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] lancer des erreurs en javascript

Pour obtenir des informations utiles sur le numéro de ligne et la pile d’appels, il faut utiliser des instances Error. Lancer des chaînes de caractères ne fonctionne pas comme en Python ou Ruby.

Bien que JavaScript apparaisse gérer throw "string", vous verrez la chaîne dans votre gestionnaire catch. Cependant, ce sera la seule donnée visible*. Aucun cadre de pile (stack frame) antérieur ne sera inclus.

Exemples corrects avec new Error :

throw new Error('message') // ✅
Promise.reject(new Error('message')) // ✅
throw Error('message') // ✅
Promise.reject(Error('message')) // ✅

Les anti‑patterns courants :

throw 'error message' // ❌
Promise.reject(-42) // ❌

#3 Gérez les erreurs là où cela a du sens

Les Promises offrent un moyen élégant de gérer les erreurs, grâce à .catch(). C’est essentiellement une variante spéciale de .then() — où les erreurs provenant des .then() précédents sont interceptées. Voyons 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'))

Même si .catch() peut rappeler un gestionnaire d’événement DOM (par ex. click, keypress), son positionnement est crucial, car il ne peut « attraper » que les erreurs situées au-dessus de lui.

Écraser les erreurs est relativement trivial : renvoyez une valeur non‑erreur dans votre fonction de rappel .catch(), et la chaîne de Promises bascule sur les callbacks .then() suivants (effectivement).

Suivez 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) // sortie attendue : 100

C’est la séquence qui importe.

Tout en restant un exemple un peu tiré par les cheveux, il sert à illustrer le flux d’erreurs et de données dans les Promises.

Voici le plan de la séquence :

  1. 42 est la valeur initiale.
  2. hello est toujours renvoyé par la méthode suivante.
  3. on ignore la valeur précédente et on lève une erreur avec le message 'totes fail'.
  4. .catch() intercepte l’erreur, renvoie à la place 99, qui sera traité par tout .then() subséquent.
  5. on incrémente num, ce qui donne 100.
  6. la fonction console.log reçoit 100 et l’affiche ! :tada:

Question : Que se passe-t-il lorsqu’on enchaîne 2 .catch() ? Le second peut‑il jamais s’exécuter ? Pouvez‑vous imaginer un cas d’usage ?

Question : Comment .catch() peut‑il ignorer des erreurs ? Comment empêcher que des erreurs provoquent une sortie prématurée de Promise.all ?

#4 Apporter de la clarté avec des fonctions nommées 🦄✨

Comparez la lisibilité des deux 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 * 2
const quarter = x => x / 4
const square = x => x * x
const 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 amis de Array.prototype. Incluant .map(), .filter(), .every(), .some(), .find() !

Les pipelines de collection #FTW :

// C'EST LA MÊME CHOSE :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) // sortie attendue sur 2 lignes : "25.00", "100.00"

Et si vous ne voulez pas écrire ce code linéaire… Vous avez simplement des fonctions !

Vous pouvez les utiliser comme bon vous semble :

// Modèle d’imbrication
// ❌ ne faites pas ça, cependant
const result = format(square(quarter(double(10))))
log(result)
// sortie attendue : "25.00"

Pourquoi l’imbrication de fonctions est‑elle un anti‑pattern ?

  1. Peu lisible pour la plupart des développeurs
  2. Les diff Git ne révèlent pas facilement qui a changé quoi
  3. Difficile à déboguer ou à logger au milieu des fonctions imbriquées