Une promesse est un objet (Promise) qui représente la complétion ou l'échec d'une opération asynchrone. La plupart du temps, on « consomme » des promesses et c'est donc ce que nous verrons dans la première partie de ce guide pour ensuite expliquer comment les créer.

En résumé, une promesse est un objet qui est renvoyé et auquel on attache des callbacks plutôt que de passer des callbacks à une fonction. Ainsi, au lieu d'avoir une fonction qui prend deux callbacks en arguments :

faireQqc(successCallback, failureCallback);

On aura une fonction qui renvoie une promesse et on attachera les callbacks sur cette promesse :

let promise = faireQqc();
promise.then(successCallback, failureCallback);

ou encore :

faireQqc().then(successCallback, failureCallback);

Cette dernière forme est ce qu'on appelle un appel de fonction asynchrone. Cette convention possède différents avantages dont le premier est le chaînage.

Garanties

À la différence des imbrications de callbacks, une promesse apporte certaines garanties :

  • Les callbacks ne seront jamais appelés avant la fin du parcours de la boucle d'évènements JavaScript courante
  • Les callbacks ajoutés grâce à then seront appelés, y compris après le succès ou l'échec de l'opération asynchrone
  • Plusieurs callbacks peuvent être ajoutés en appelant then plusieurs fois, ils seront alors exécutés dans un ordre indépendant de l'ordre d'insertion.

Chaînage des promesses

La méthode then() renvoie une nouvelle promesse, différente de la première :

let promise = faireQqc();
let promise2 = promise.then(successCallback, failureCallback);

ou encore :

let promise2 = faireQqc().then(successCallback, failureCallback);

La deuxième promesse indique l'état de complétion, pas uniquement pour faireQqc() mais aussi pour le callback qui lui a été passé (successCallback ou failureCallback) qui peut aussi être une fonction asynchrone qui renvoie une promesse. Lorsque c'est le cas, tous les callbacks ajoutés à promise2 forment une file derrière la promesse renvoyée par successCallback ou failureCallback.

Autrement dit, chaque promesse représente l'état de complétion d'une étape asynchrone au sein de cette succession d'étapes.

Auparavant, l'enchaînement de plusieurs opérations asynchrones déclenchait une pyramide dantesque de callbacks :

faireQqc(function(result) {
  faireAutreChose(result, function(newResult) {
    faireUnTroisiemeTruc(newResult, function(finalResult) {
      console.log('Résultat final :' + finalResult);
    }, failureCallback);
  }, failureCallback);
}, failureCallback);

Grâce à des fonctions plus modernes et aux promesses, on attache les callbacks aux promesses qui sont renvoyées. On peut ainsi construire une chaîne de promesses :

faireQqc().then(function(result) {
  return faireAutreChose(result);
})
.then(function(newResult) {
  return faireUnTroisiemeTruc(newResult);
})
.then(function(finalResult) {
  console.log('Résultat final : ' + finalResult);
})
.catch(failureCallback);

Les arguments passés à then sont optionnels. La forme catch(failureCallback) est un alias plus court pour then(null, failureCallback). Ces chaînes de promesses sont parfois construites avec des fonctions fléchées :

faireQqc()
.then(result => faireAutreChose(result))
.then(newResult => faireUnTroisiemeTruc(newResult))
.then(finalResult => {
  console.log('Résultat final : ' + finalResult);
})
.catch(failureCallback);

Important : cela implique que les fonctions asynchrones renvoient toutes des promesses, sinon les callbacks ne pourront être chaînés et les erreurs ne seront pas interceptées (les fonctions fléchées ont une valeur de retour implicite si les accolades ne sont pas utilisées).

Chaînage après un catch

Il est possible de chaîner de nouvelles actions après un rejet, c'est à dire un catch. C'est utile pour accomplir de nouvelles actions après qu'une action ait échoué dans la chaine. Par exemple :

new Promise((resolve, reject) => {
    console.log('Initial');

    resolve();
})
.then(() => {
    throw new Error('Something failed');
        
    console.log('Do this');
})
.catch(() => {
    console.log('Do that');
})
.then(() => {
    console.log('Do this whatever happened before');
});

Cela va produire la sortie suivante :

Initial
Do that
Do this whatever happened before

Notez que le texte Do this n'est pas affiché car l'erreur Something failed a produit un rejet.

Propagation des erreurs

Dans les exemples précédents, failureCallback était présent trois fois dans la pyramide de callbacks et une seule fois, à la fin, dans la chaîne des promesses :

faireQqc()
.then(result => faireAutreChose(value))
.then(newResult => faireUnTroisiemeTruc(newResult))
.then(finalResult => console.log('Résultat final : ' + finalResult))
.catch(failureCallback);

En fait, une chaîne de promesses s'arrête dès qu'il y a une exception pour arriver à la méthode catch(). Ce fonctionnement est assez proche de ce qu'on peut trouver pour du code synchrone :

try {
  let result = syncFaireQqc();
  let newResult = syncFaireQqcAutre(result);
  let finalResult = syncFaireUnTroisiemeTruc(newResult);
  console.log('Résultat final : ' + finalResult);
} catch(error) {
  failureCallback(error);
}

Cette symétrie entre le code asynchrone et le code synchrone atteint son paroxysme avec le couple d'opérateurs async/await d'ECMAScript 2017:

async function toto() {
  try {
    let result = await faireQqc();
    let newResult = await faireQqcAutre(result);
    let finalResult = await faireUnTroisiemeTruc(newResult);
    console.log('Résultat final : ' + finalResult);
  } catch(error) {
    failureCallback(error);
  }
}

Ce fonctionnement est construit sur les promesses et faireQqc() est la même fonction que celle utilisée dans les exemples précédents.

Les promesses permettent de résoudre les problèmes de cascades infernales de callbacks notamment en interceptant les différentes erreurs (exceptions et erreurs de programmation). Ceci est essentiel pour obtenir une composition fonctionnelle des opérations asynchrones.

Envelopper les callbacks des API

Il est possible de créer un objet  Promise grâce à son constructeur. Et même si cela ne devrait pas être nécessaire, certaines API fonctionnent toujours avec des callbacks passés en arguments. C'est notamment le cas de la méthode  setTimeout() :

setTimeout(() => saySomething("10 seconds passed"), 10000);

Si on mélange des callbacks et des promesses, cela sera problématique. Si  saySomething échoue ou contient des erreurs, rien n'interceptera l'erreur.

Pour ces fonctions, la meilleure pratique consiste à les envelopper dans des promesses au plus bas niveau possible et de ne plus les appeler directement :

let wait = ms => new Promise(resolve => setTimeout(resolve, ms));

wait(10000).then(() => saySomething("10 seconds")).catch(failureCallback);

Le constructeur Promise prend en argument une fonction et nous permet de la convertir manuellement en une promesse. Ici, vu que setTimeout n'échoue pas vraiment, on laisse de côté la gestion de l'échec.

Composition

Promise.resolve() et Promise.reject() sont des méthodes qui permettent de créer des promesses déjà tenues ou rompues.

Promise.all() et Promise.race() sont deux outils de composition qui permettent de mener des opérations asynchrones en parallèle.

Il est possible de construire une composition séquentielle de la façon suivante :

[func1, func2].reduce((p, f) => p.then(f), Promise.resolve());

Dans ce fragment de code, on réduite un tableau de fonctions asynchrones en une chaîne de promesse équivalente à : Promise.resolve().then(func1).then(func2);

On peut également accomplir cela avec une fonction de composition réutilisable  :

let applyAsync = (acc, val) => acc.then(val);
let composeAsync = (...funcs) => x => funcs.reduce(applyAsync, Promise.resolve(x));

La fonction composeAsync accepte autant de fonctions que nécessaire comme arguments et renvoie une nouvelle fonction qui prend une valeur initiale pour la passer à travers ces étapes de compositions. Cette façon de faire garantit que les fonctions, qu'elles soient synchrones ou asynchrones, sont exécutées dans le bon ordre :

let transformData = composeAsync(func1, asyncFunc1, asyncFunc2, func2);
transformData(data);

Avec ECMAScript 2017, on peut obtenir une composition séquentielle plus simplement avec les opérateurs await/async :

for(let f of [func1, func2]) {
  await f();
} 

Gestion du temps

Pour éviter de mauvaises surprises, les fonctions passées à then() ne seront jamais appelées de façon synchrone, y compris lorsqu'il s'agit d'une promesse déjà résolue :

Promise.resolve().then(() => console.log(2));
console.log(1); // 1, 2

En fait, la fonction passée à then() est placée dans une file de micro-tâches qui sont exécutées lorsque cette file est vidée à la fin de la boucle d'évènements JavaScript :

var wait = ms => new Promise(resolve => setTimeout(resolve, ms));

wait().then(() => console.log(4));
Promise.resolve().then(() => console.log(2)).then(() => console.log(3));
console.log(1); // 1, 2, 3, 4

Voir aussi

Étiquettes et contributeurs liés au document

 Contributeurs à cette page : SphinxKnight, madarche
 Dernière mise à jour par : SphinxKnight,