Une promise, c'est un espace réservé pour une valeur future
Quand JavaScript doit effectuer une opération qui prend du temps — une requête réseau, la lecture d'un fichier, l'attente d'un timer — il ne peut pas te renvoyer le résultat immédiatement. À la place, il te retourne une promise : un objet qui représente une valeur qui finira par exister.
Le premier console.log affiche une Promise en attente. Une demi-seconde plus tard, la Promise se résout et le callback du .then s'exécute avec la valeur. La Promise en elle-même n'est qu'un objet ; ce qui est magique, c'est qu'elle sait prévenir quiconque l'écoute dès que sa valeur est disponible.
Les trois états d'une promise
Une promise javascript se trouve toujours dans l'un de ces trois états :
- pending (en attente) — le travail est en cours. Aucune valeur pour le moment.
- fulfilled (tenue) — le travail a réussi. Une valeur est disponible.
- rejected (rejetée) — le travail a échoué. Une erreur est disponible.
Une Promise passe de pending à fulfilled ou à rejected une seule fois, puis reste figée dans cet état pour toujours. Impossible d'annuler la résolution d'une Promise ou de la résoudre deux fois.
Promise.resolve(value) crée une Promise déjà résolue ; Promise.reject(error) en crée une déjà rejetée. Pratique pour les tests, ou pour renvoyer une Promise depuis une fonction qui a parfois la réponse tout de suite.
Lire la valeur : .then et .catch
On ne récupère pas la valeur d'une Promise directement — il faut passer un callback à .then, et la Promise l'appellera dès que la valeur sera prête :
.catch(fn) s'exécute quand la Promise est rejetée. En interne, c'est en fait un raccourci pour .then(undefined, fn). Un seul .catch() placé à la fin de la chaîne attrape les rejets de n'importe quelle étape précédente — pas besoin d'en coller un après chaque .then.
Chaînage de promises : chaque .then renvoie une nouvelle Promise
C'est là que beaucoup de gens se cassent les dents. .then() ne se contente pas d'exécuter un callback : il retourne une nouvelle Promise qui se résout avec la valeur renvoyée par ce callback. C'est justement ce qui rend le chaînage possible :
Chaque étape alimente la suivante. Si un callback .then renvoie une Promise, le chaînage attend cette Promise avant de poursuivre — ce qui permet d'enchaîner proprement les étapes asynchrones :
Trois étapes asynchrones enchaînées, sans la moindre imbrication. Comparez avec la même logique écrite à coups de callbacks et vous comprendrez tout de suite pourquoi les Promises ont autant cartonné.
Les erreurs traversent toute la chaîne
Une Promise rejetée saute tous les .then jusqu'à tomber sur un .catch. Voilà, c'est tout le modèle de gestion d'erreurs des promises en JavaScript :
Lancer une exception à l'intérieur d'un .then rejette la Promise renvoyée par ce .then. Le .then suivant voit le rejet et le fait suivre, jusqu'à ce qu'un .catch l'attrape. En général, un seul .catch en bout de chaîne suffit amplement — et une chaîne sans aucun .catch déclenchera un avertissement « unhandled promise rejection », qu'il vaut mieux corriger.
Créer sa propre promise avec new Promise
La plupart du temps, tu vas simplement consommer des Promises fournies par des bibliothèques. De temps en temps, il faut en emballer une toi-même autour d'un truc qui n'en renvoie pas — typiquement une vieille API à base de callbacks :
La fonction que tu passes à new Promise s'appelle l'executor. Elle reçoit deux arguments : resolve (à appeler avec la valeur de succès) et reject (à appeler avec une erreur). Il faut en appeler un, et un seul, une seule fois. Les appels suivants sont tout simplement ignorés.
Deux réflexes qui évitent bien des galères :
- N'utilise
new Promiseque pour emballer quelque chose qui n'est pas déjà basé sur une Promise. Si une fonction renvoie déjà une Promise, contente-toi de la retourner. - Passe toujours un objet
Erroràreject, jamais une simple chaîne de caractères. La stack trace, ça se garde précieusement.
Exécuter plusieurs tâches en parallèle avec Promise.all
Les chaînes de .then s'exécutent les unes après les autres. Mais quand tu as plusieurs tâches asynchrones indépendantes et que tu veux les lancer en même temps, Promise.all est l'outil parfait :
Les trois timers tournent en parallèle. Promise.all renvoie un tableau de résultats dans le même ordre que les entrées — une fois que chaque Promise est résolue. Le temps total tourne autour de 400 ms, pas 900 ms.
Le piège : Promise.all rejette dès qu'une seule des Promises échoue, et les autres résultats partent à la poubelle. C'est le comportement qu'on veut quand on a besoin de toutes les pièces (par exemple, afficher une page qui dépend de trois appels API). Sinon, passez plutôt par allSettled.
Quand certains échecs sont acceptables : Promise.allSettled
Promise.allSettled attend que toutes les Promises se terminent — qu'elles soient tenues ou rejetées — et vous livre un rapport complet :
Chaque résultat est un objet de la forme { status: "fulfilled", value } ou { status: "rejected", reason }. C'est très pratique quand un succès partiel te suffit : logger un lot d'événements, récupérer une série de miniatures, lancer plusieurs health checks indépendants.
Deux autres combinateurs valent le coup d'œil :
Promise.race([...])— se règle dès que la première Promise se règle, peu importe le résultat. Parfait pour gérer des timeouts.Promise.any([...])— renvoie la première qui réussit et ignore les rejets. Ne rejette que si toutes les Promises échouent.
Les promises sont toujours asynchrones
Même une Promise déjà résolue appelle son callback .then de façon asynchrone — jamais en synchrone, jamais dans le même tick :
Le résultat affiché est avant, après, immédiat. Le callback passé à .then attend que le code en cours se termine, puis s'exécute via la file des microtâches. Cette règle — « un callback de Promise ne s'exécute jamais de façon synchrone » — explique pourquoi mélanger des promises avec du code synchrone reste prévisible : le code synchrone passe toujours en premier.
La suite : async/await
Enchaîner les .then fonctionne très bien, mais dès qu'on dépasse deux ou trois étapes, ça prend vite des airs d'escalier. async/await est une syntaxe bâtie par-dessus les promises qui permet d'écrire la même logique comme si elle était synchrone — avec try/catch pour gérer les erreurs et de simples variables pour stocker les valeurs intermédiaires. On attaque ça juste après.
Questions fréquentes
C'est quoi une Promise en JavaScript ?
Une Promise est un objet qui représente une valeur pas encore disponible — typiquement le résultat futur d'une opération asynchrone, comme un appel réseau. Elle se trouve toujours dans l'un de ces trois états : pending, fulfilled ou rejected. Pour récupérer la valeur finale, on attache des callbacks avec .then() et .catch().
Quelle différence entre then et catch ?
.then(onFulfilled) s'exécute quand la Promise est résolue avec succès et reçoit la valeur résolue. .catch(onRejected) se déclenche si la Promise (ou n'importe quelle Promise plus haut dans la chaîne) est rejetée et reçoit l'erreur. Un seul .catch() en bout de chaîne suffit à attraper les erreurs de toutes les étapes précédentes.
À quoi sert Promise.all ?
Promise.all([p1, p2, p3]) prend un tableau de Promises et renvoie une seule Promise qui se résout avec le tableau des valeurs — mais uniquement une fois que toutes les Promises en entrée sont résolues. Si une seule échoue, l'ensemble est rejeté immédiatement. Si tu veux récupérer tous les résultats même en cas d'échec, passe par Promise.allSettled.
Faut-il utiliser les Promises ou async/await ?
C'est la même mécanique sous le capot — async/await n'est qu'une syntaxe posée sur les Promises. Le code récent est généralement plus lisible avec async/await, mais une fonction async renvoie toujours une Promise, on gère les erreurs avec try/catch ou .catch(), et on garde Promise.all pour lancer des traitements en parallèle. Bien comprendre les Promises, c'est ce qui rend async/await évident.