Comprendre les promesses en JavaScript
Avec EcmaScript 2015 est apparue une notion aussi complexe que puissante : les promesses. Elles nous permettent d'améliorer la maintenabilité de notre code d'une part, mais également de faciliter sa lisibilité tout en optimisant la robustesse de notre application. C'est parti pour un tour d'horizon des promesses en JavaScript !
Nous parlerons beaucoup de callbacks durant ce chapitre, un callback (en français "fonction de rappel") est une fonction qui réagit à un évènement. Voici un exemple de callback appelée suite à un événement utilisateur "click" sur un bouton :
document.querySelector("#mon_bouton").addEventListener(function() { // Callback
// Cette fonction anonyme est ce qu'on appelle un callback car elle permet de réagir à un événement, ici le click utilisateur
console.log("Vous avez cliqué sur le bouton !")
});
Problème : Cascade de callbacks
Ce principe représente une mauvaise pratique de développement où l'on se retrouve avec une succession de callbacks imbriqués : scénario récurrent en JavaScript. Prenons un exemple simple, vous souhaitez :
- Réagir au click sur un bouton ;
- Pour appeler ensuite un serveur afin de lui envoyer une donnée ;
- Puis afficher un message de félicitations à votre utilisateur au bout de 10 secondes.
On pourrait représenter ce code ainsi :
document.querySelector("#mon_bouton").addEventListener(() => { // Etape 1
// Cette fonction appelle le backend, on lui passe en paramètre la fonction à appeler une fois la réponse fournit par notre API
callBackend('/mes-cadeaux-de-noel', (cadeaux) => { // Etape 2
setTimeout(() => { // Etape 3
alert(`Avec un peu d'attente, voici enfin vos cadeaux de Noël -> ${cadeaux}`);
}, 10000); // 10000ms = 10s
});
});
Une bonne pratique JavaScript consiste à implémenter des arrow functions dès lors qu'il s'agit de callbacks !
Yep, ça marche plutôt bien mais vous la voyez la cascade ?

Oui celle-là, c'est la mauvaise pratique dont je vous parlais. Ce genre de développement rend notre code difficile à lire et à maintenir pour une raison simple : toute la logique est dans un même bloc. On a donc du mal à démêler les différentes tâches, c'est un peu similaire à ça :

Cette manière de développer avec la succession des conditions imbriquées porte un nom qui annonce la couleur: 🔺Pyramid of doom🔺 - en français la "pyramide condamnée" 😨 Franchement pas rassurant, on va préférer éviter.
"Pas de soucis" me direz-vous, "il n'y a qu'à factoriser ça et passer par des fonctions". Oui, mais là encore, pensez au développeur qui arrive après vous et qui devra scroller ou naviguer d'un fichier à l'autre pour comprendre la logique des fonctions qui se succèdent - c'est possible mais c'est loin d'être le top. Non, ce qu'on veut c'est des blocs qui se suivent, qui ne sont pas imbriqués l'un dans l'autre et où chacun possède sa propre logique ! Eh bah ça tombe bien, c'est ce que les promesses nous proposent 😸
Les promesses
Comme énoncé plus haut, les promesses vont nous permettre d'aplatir le code, d'éviter l'imbrication de callbacks au profit de blocs successifs :
Cascade de callbacks | Promesses |
![]() | ![]() |
Vous allez voir, le principe d'une promesse est simple à comprendre. Imaginons un scénario avec 2 entités A et B que l'on nommera Alice et Bob 👩🏻🤝🧑🏼
Bob souhaite organiser un dîner et compte sur Alice pour faire les courses. Alice promet donc à Bob de revenir vers lui plus tard, elle ne sait ni combien de temps elle mettra ni si elle pourra acheter toute la liste mais la promesse est faite. Bob acquiesce et attend patiemment son retour — dès lors, 2 scénarios s'imposent à nous :
- Alice revient les mains pleines et Bob peut préparer un bon petit plat ;
- Alice revient bredouille et Bob devra donc annoncer à ses convives que le dîner est malheureusement annulé.
Vous l'avez compris, il s'agira pour nous de gérer 2 cas de figure bien distincts : réussite ou échec.
Super ! Tout ça c'est bien beau mais à part nous ouvrir l'appétit on y voit pas beaucoup plus clair. Vrai, représentons donc "tout ça" avec du code :
// Entité A
const promise_alice = new Promise((resolve, reject) => {
setTimeout(() => { // On simule le retour d'un serveur avec l'attente d'une seconde
resolve({ // C'est l'objet qu'Alice va fournir à Bob
fraises: 3,
chantilly: 1,
noix_macadamia: 4
})
}, 1000);
})
// Entité B
promise_alice.then((sacDeCourse) => {
console.log("Merci !");
});
// Ce code va être exécuté avant la fin de la promesse :)
console.log("Une promesse n'est pas bloquante !");
La classe Promise est fournie par JavaScript, c'est elle qui nous permet de créer la promesse permettant à l'entité B de réagir
La promesse prend en paramètre une fonction qui elle-même prend 2 paramètres : resolve et reject — resolve c'est le drapeau vert à lever pour indiquer à Bob que tout s'est bien passé, reject quant à lui signifie qu'un problème est survenu. Dans notre fonction représentant la promesse, on y implémente une fonction setTimeout (nous permet ici de simuler l'attente d'un serveur). Une fois son callback arrivé à terme, on appelle resolve avec en paramètre l'objet contenant nos courses.
Une fois la promesse A générée, il s'agira de créer l'entité B afin qu'elle attende patiemment la fin de la promesse. Le .then
prend en paramètre un callback permettant d'indiquer comment réagir une fois la promesse tenue. À noter qu'une promesse est asynchrone, la ligne 18 s'affichera donc avant que Bob ne réagisse !
Les noms des paramètres "resolve" et "reject" importe peu, vous pouvez attribuer le nom qui vous chante, seul l'ordre est important ici. Le premier est à appeler en cas de réussite et le second en cas d'échec
Revenons à notre programme initial, comment faire pour obtenir une écriture linéaire ? Ici, pour réagir avec un autre événement asynchrone derrière, on pourrait peut-être faire :
[...]
// CE CODE N'EST PAS BON !
// Entité B
promise_alice.then((sacDeCourse) => {
console.log("Merci, mais il me manque une fraise...");
const nouvelle_promesse = new Promise((resolve, reject) => {
resolve({
fraises: 1
})
});
nouvelle_promesse.then(() => {
// Suite du programme
});
});
Malheureusement, on revient petit à petit à notre cascade, rappelez-vous, celle que l'on veut absolument éviter. Vous comprenez donc qu'il ne s'agit pas de la bonne manière de faire, les promesses nous propose une alternative simple et efficace : le chaînage !
Chaîner les promesses
Les promesses sont dites "chaînables", on peut imaginer la chose comme une course de relais - Un bloc .then
peut renvoyer une promesse qui pourra être récupérée par le prochain .then
à son tour et ainsi de suite ! Commençons par factoriser notre code précédent :
function faireLesCoursesPromise() {
const promise_alice = new Promise((resolve, reject) => {
setTimeout(() => { // On simule le retour d'un serveur avec l'attente d'une seconde
resolve({
fraises: 3,
chantilly: 1,
noix_macadamia: 4
})
}, 1000);
});
return promise_alice;
}
faireLesCoursesPromise().then((sacDeCourse) => {
// Bob réagit
});
Une fois la promesse récupérée, l'entité B est capable à son tour de renvoyer une promesse. Il passe le relais au coureur prochain qui attend patiemment que le bloc d'avant ait terminé sa course :
[...]
faireLesCoursesPromise()
.then((sacDeCourse) => {
console.log("Il n'y en a pas assez, il faut y retourner...");
return faireLesCoursesPromise();
})
.then((sacDeCourseComplet) => {
console.log("C'est tout bon, merci :D");
})
;
À noter que l'on peut chaîner les promesses autant de fois qu'on le souhaite. Et c'est ainsi qu'on arrive à éviter la cascade de callback :
faireLesCoursesPromise()
.then((sacDeCourse) => { // Coureur 1
console.log("Il n'y en a pas assez, il faut y retourner...");
return faireLesCoursesPromise();
})
.then((sacDeCourseComplet) => { // Coureur 2
console.log("Merci, je m'occupe de réaliser la glace");
return faireLaGlacePromise();
})
.then((glace) => { // Coureur 3
console.log("Je l'envoie au serveur");
return envoyerAuServeur();
})
.then((retourDuServeur) => { // Coureur 4
console.log("La glace a été correctement enregistrée côté serveur, fin du programme.")
})
;
Une promesse renvoie toujours une promesse
Qu'on le veuille ou non, l'entité B renverra une promesse - tout le temps ! Même lorsque votre return
ne renvoie pas de promesse, Javasript réalisera une conversion automatique. Il nous sera donc possible d'écrire un code comme suit :
faireLesCoursesPromise()
.then((sacDeCourse) => {
console.log("Il n'y en a pas assez, il faut y retourner...");
return faireLesCoursesPromise();
})
.then((sacDeCourseComplet) => {
console.log("Merci, je m'occupe de préparer la glace");
// On renvoie non pas une promesse mais un objet directement
return {
glaceQuantite: 1
};
})
.then((glace) => {
console.log("Ca a été plus rapide que prévu, merci !");
// Ici, il n'y a carrément pas de return. Ce sera convertit par return new Promise((resolve, reject) => resolve());
})
.then((valeur) => { // valeur = undefined !
console.log("La glace a été correctement enregistrée et consommée, fin du programme.")
})
;
Le dernier
.then
récupérera un élémentundefined
car JavaScript remplace le retour du bloc précédent parreturn new Promise((resolve, reject) => resolve());
Pas mal tout ça, mais concrètement, quand est-ce qu'une promesse est appelée ?
Une promesse n'est pas lazy
La promesse est-elle appelée dès sa création ? Attend-elle qu'une entité B l'écoute (.then
) afin de s'enclencher ? Tentons par exemple une approche comme celle-ci :
// A
const unePromesse = new Promise((resolve, reject) => {
console.log("Promesse enclenchée");
setTimeout(() => {
console.log("Promesse terminée");
resolve(42);
}, 1500); // 1,5 seconde
});
// On crée l'entité B au bout de 2 secondes
setTimeout(() => {
unePromesse.then((res) => { // B
console.log("Entité B réagit");
console.log(res); // 42
})
}, 2000);

À l'affichage en console, on se rend compte que la promesse est appelée dès le départ et n'attend pas qu'on l'écoute pour démarrer, on dit d'une promesse qu'elle n'est pas lazy (fainéante). Elle s'enclenche automatiquement au moment de sa création !
On a donc résolu haut la main le problème de l'écriture en cascade, mais faisons quelques pas en arrière et souvenons-nous du début de ce chapitre. Nous avions établi qu'il existait 2 scénarios possibles à une promesse, la réussite représentée par resolve et l'échec avec reject. Que se passerait-il si on appelait reject plutôt que resolve ?
Rendez vos promesses robustes
En appelant reject, on indique au programme que l’exécution de la promesse ne s'est pas déroulée comme prévu. Cela peut arriver dans le cas où votre serveur ne renvoie pas les informations nécessaires, auquel cas il faudra effectuer un traitement différent, peut-être en affichant un message d'erreur adapté selon le problème remonté. Dans notre scénario à nous : un scénario d'échec serait qu'Alice n'a pas pu faire les courses correctement. Il est évident que Bob ne réagira pas de la même manière car notre glace ne peut être faite sans les bons ingrédients! Pour capturer ce scénario d'échec, il va être nécessaire d'ajouter un bloc .catch
comme ci-dessous :
[...]
function faireLesCoursesPromise() {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject('Magasin fermé :(');
}, 1000);
});
}
faireLesCoursesPromise()
.then((sacDeCourse) => {
console.log("Il n'y en a pas assez, il faut y retourner..."); // Pas affiché
return faireLesCoursesPromise();
})
.then((sacDeCourseComplet) => {
console.log("Merci, je m'occupe de réaliser la glace"); // Pas affiché
return faireLaGlacePromise();
})
.then((glace) => {
console.log("Je l'envoie au serveur"); // Pas affiché
return envoyerAuServeur();
})
.then((retourDuServeur) => {
console.log("La glace a été correctement enregistrée côté serveur, fin du programme."); // Pas affiché
})
.catch((err) => {
console.log("Erreur lors de l'éxecution de la promesse.");
console.error(err);
})
;

Tous les .then sont bypassés pour arriver au bloc catch qui lui, est interprété
Ce bloc .catch
est bien plus puissant qu'il n'y paraît, il permet à la fois de capturer l'erreur lors de la création de la promesse (scénario d'échec venant d'Alice) mais également de capturer la moindre erreur levée par n'importe quel coureur par la suite (comprenez les blocs .then
successifs). Le bloc catch est donc absolument nécessaire pour rendre vos promesses robustes.
Une promesse levant une erreur et qui n'a pas de bloc catch provoquera l’exécution d'un événement JavaScript
unhandledrejection
. Cet événement peut être capturé et un traitement générique pourra être appliqué par le développeur - Plus d'informations ici
Comment faire pour capturer une erreur en particulier ?
Bonne question, il est évident que dans certains cas, nous voudrons gérer nos erreurs de manière granulaire — bloc par bloc.
Auquel cas, le .catch
englobant tout le monde n'est plus du tout adapté ! On veut déterminer si le problème vient du click utilisateur, du serveur ou de l'affichage du message qui sont 3 blocs bien distincts. Pour y arriver, on peut appliquer un .catch
spécifique à chacun des .then
comme ceci :
[...]
faireLesCoursesPromise()
.then((sacDeCourse) => { // Coureur 1
console.log("Il n'y en a pas assez, il faut y retourner...");
return faireLesCoursesPromise();
})
.catch((err) => {
console.error('Gestion erreur pour coureur numéro 1');
})
.then((sacDeCourseComplet) => { // Coureur 2
console.log("Merci, je m'occupe de réaliser la glace");
return faireLaGlacePromise();
})
.catch((err) => {
console.error('Gestion erreur pour coureur numéro 2');
})
.then((glace) => { // Coureur 3
console.log("Je l'envoie au serveur");
})
.catch((err) => {
console.error('Gestion erreur pour coureur numéro 3');
})
;
Comme vous pouvez le constater, le bloc
.catch
est chaînable (comme un.then
). Ce qui signifie qu'une fois l'erreur levée, le coureur.catch
prend le relais, effectuera le traitement et passera le relais au.then
suivant
À noter que le code précédent est à peu près équivalent au code ci-dessous :
[...]
faireLesCoursesPromise()
.then((sacDeCourse) => { // Coureur 1
console.log("Il n'y en a pas assez, il faut y retourner...");
return faireLesCoursesPromise();
}, (err) => {
console.error("Gestion erreur venant d'Alice");
})
.then((sacDeCourseComplet) => { // Coureur 2
console.log("Merci, je m'occupe de réaliser la glace");
return faireLaGlacePromise();
}, (err) => {
console.error('Gestion erreur pour coureur numéro 1');
})
.then((glace) => { // Coureur 3
console.log("Je l'envoie au serveur");
}, (err) => {
console.error('Gestion erreur pour coureur numéro 2');
})
.then(() => {}, (err) => {
console.error('Gestion erreur pour coureur numéro 3');
})
;
Je sais pas trop pour vous mais moi je préfère la première écriture avec les .catch
, et de loin !
Après les blocs .then
et .catch
, il y en a un dernier qui est apparu un petit peu tardivement (avec EcmaScript 2017), c'est le .finally
. Celui-ci va nous permettre de réagir dans le cas ou tout s'est bien passé comme dans le cas où tout s'est mal déroulé, en gros - tout le temps ! Bien pratique quand il s'agit d'appliquer un comportement commun comme le fait de cacher la barre de chargement suite une requête :
- Si le serveur répond correctement, il faut retirer le chargement pour afficher la donnée ;
- Si une erreur est survenue, il faut également le retirer pour afficher l'erreur.
Voici comment implémenter ce nouveau bloc :
[...]
faireLesCoursesPromise()
.then((sacDeCourse) => {
console.log("Executé en cas de réussite");
})
.catch((err) => {
console.error("Executé en cas d'échec");
})
.finally(() => {
console.log("Executé tout le temps à la fin");
});
Bon bah voilà, on a fait le tour des promesses. L'air de rien, elles nous ont permis de grandement gagner en lisibilité comparativement à la cascade de callbacks tout en gérant de manière efficace les erreurs potentielles. C'est pas mal tout ça, mais nous développeurs, on a quand même réussi à trouver le moyen de râler et avons exigé une écriture bien plus simple encore !
Async et Await
Je vous avais promis une écriture totalement linéaire et avec les promesses, on n'est pas loin du tout ! Maiiis on y est pas 😩️ C'est plutôt une écriture en dents de scie que l'on a. Le scénario optimal serait d'avoir une écriture synchrone avec un comportement asynchrone et vous savez quoi ? C'est possible depuis EcmaScript 2017 !
2 petits mots-clés vont nous permettre d'y arriver : async et await. Ce serait extrêmement pratique si on pouvait écrire notre scénario initiale comme ceci :
const sacDeCourse = faireLesCoursesPromise();
console.log(sacDeCourse); // [object Promise]... Pas bon !
Ce code ne fonctionnera malheureusement pas comme prévu et cela pour une raison simple : JavaScript est de nature asynchrone et n'attendra pas le retour de la promesse avant d’exécuter la suite du code. On récupérera donc une promesse en cours (pending
) dans le console.log
. L'équipe JavaScript a développé un moyen pour que l'on puisse attendre le retour d'une promesse avant de passer à la ligne de code suivante et ceci, sans le moindre bloc .then
. La condition étant que notre code doit impérativement se trouver dans un contexte asynchrone. Pour créer ce contexte asynchrone, il suffit simplement de créer une fonction et de rajouter le petit mot-clé async
avant :
async function maFonctionAsynchrone() {
return 42;
}
Le mot-clé async
va permettre de transformer notre fonction automatiquement en promesse. Désormais pour récupérer le résultat de la fonction, nous serons obligé de passer par un .then
:
async function maFonctionAsynchrone() {
return 42;
}
maFonctionAsynchrone().then((resultat) => {
console.log(resultat); // 42
});
C'est un petit peu contraignant je vous l'accorde, mais le jeu en vaut largement la chandelle. Voyons maintenant comment déclarer nos promesses à l'intérieur :
async function maFonctionAsynchrone() {
const courses = await faireLesCoursesPromise(); // 1
const nouvellesCourses = await faireLesCoursesPromise(); // 2
const glace = await faireLaGlacePromise(); // 3
const retourServeur = await envoyerAuServeurPromise(); // 4
return true; // Quand tout est terminé, on renvoie true !
});
maFonctionAsynchrone().then((res) => {
console.log(res); // true
console.log("Glace enregistrée côté serveur, on est bon !");
});
Vous le voyez ci-dessus, nul besoin de bloc .then. Le simple mot-clé await
permet d'indiquer à JavaScript qu'il faut attendre le retour de la promesse avant d’exécuter la suite.
Le code ne devient pas bloquant avec async / await !
On garde évidemment le comportement asynchrone des promesses, cette écriture await
va être converti automatiquement par JavaScript par une succession de blocs .then
.
Maintenant qu'il n'y a plus de blocs
.then
, nous n'avons plus de.catch
non plus. Il va être nécessaire de passer par nos bons vieux blocstry {} catch {}
pour capturer nos erreurs.
Limite des promesses
On a vu à quel point les promesses sont utiles mais il serait malhonnête de ma part de ne pas vous citer les quelques limites que l'on a avec. La première et qui n'est pas des moindre, c'est l'impossibilité d'implémenter un système de timeout (limite de temps). Notre ami Bob est très patient - si Alice met 1 heure à répondre, il attendra patiemment et entièrement ladite heure. Notre client n'aura pas cette patience, il va donc être important dans certains cas d'implémenter une gestion du temps d'attente :
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('PS 5');
}, 3600000); // 1 heure !!!
// Timeout manuel
setTimeout(() => {
reject('Timeout.');
}, 5000); // 5s
});
promise
.then((res) => {
console.log("Cher client, c'était long mais voici votre lot : " + res);
})
.catch((err) => {
console.error(err);
console.log("Oops, malheureusement un problème est survenu");
});

Autre problème qui se pose à nous, comment faire pour annuler une promesse de l’extérieur ? Bah oui, imaginez qu'on ait un traitement quelconque à faire et qu'en plein milieu, votre client décide de l'annuler ?
Malheureusement à l'heure actuelle, une promesse n'est pas annulable de l'extérieur. Mais une bonne pratique va nous permettre de limiter l'effet ravageur d'une telle lacune — Le traitement doit être réalisé dans l'entité B et non pas directement dans la promesse, nous aurons une meilleure flexibilité de code et il sera possible de gérer le scénario d'annulation. Oui, c'est loin d'être optimal mais c'est tout ce que l'on a pour le moment, espérons que les prochaines versions EcmaScript corrigent ça :)
En guise de conclusion, on va essayer de rappeler à quel point les promesses sont bénéfiques pour améliorer à la fois la maintenabilité et la robustesse de votre application Web — L'écriture linéaire fournie couplée au chaînage des promesses va nous permettre de séparer nos logiques en différents blocs bien distincts, où chacun pourra utiliser le résultat du bloc précédent et ainsi de suite. Écriture que l'on peut d'ailleurs davantage simplifier avec l'implémentation des mots-clés async & await. Enfin, la mise en place des blocs .catch
nous permet de rendre nos promesses robustes et de proposer ainsi au client une application solide qui réagira à tous les scénarios : réussite comme échec.
Vous avez encore des questions sur les promesses après avoir lu cet article ? N'hésitez pas à me les poser en commentaire, j'y répondrai avec plaisir 😉️