Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 4

Handling Asynchronous Operations with JavaScript Promises

Recommended resources

Bonjour ! Bienvenue dans cette nouvelle leçon sur les fondamentaux de Node.js.

Introduction

Dans la leçon précédente, nous avons découvert le cœur de Node.js : son modèle d'exécution asynchrone non-bloquant et la boucle d'événements (Event Loop). Nous avons compris pourquoi et comment Node.js gère efficacement des milliers d'opérations simultanées avec un seul thread.

Aujourd'hui, nous passons de la théorie à la pratique. Nous allons apprendre à maîtriser l'outil principal pour gérer ces opérations asynchrones : les Promises (Promesses). Pour vous qui venez de l'environnement Laravel/PHP, où le code s'exécute souvent de manière séquentielle, les Promises représentent un concept fondamental pour écrire du code Node.js moderne et robuste. Elles sont la solution élégante au "callback hell" (l'enfer des callbacks), un problème qui rendait le code asynchrone difficile à lire et à maintenir.

À la fin de cette leçon, vous serez capable d'utiliser les Promises pour enchaîner des opérations asynchrones de manière claire, gérer les succès comme les erreurs, et exécuter plusieurs tâches en parallèle.


1. Qu'est-ce qu'une Promise ?

Une Promise est un objet JavaScript qui sert de placeholder (substitut) pour une valeur qui n'est pas encore disponible. C'est la promesse qu'une opération asynchrone se terminera un jour et vous donnera un résultat (soit une valeur de succès, soit une erreur).

Pensez à une commande dans un restaurant :

  1. Vous passez commande et recevez un ticket. Ce ticket est votre Promise.
  2. Pendant que la cuisine prépare votre plat, votre Promise est à l'état pending (en attente).
  3. Finalement, deux issues sont possibles :
    • Votre plat arrive : la Promise est fulfilled (tenue) avec le plat comme valeur.
    • La cuisine est en rupture de stock : la Promise est rejected (rejetée) avec une explication (l'erreur).

Une fois la Promise "tenue" ou "rejetée", son état est définitif. On dit qu'elle est settled (réglée).

JavaScript Promise Lifecycle Diagram
Ce diagramme illustre les trois états d'une Promise : elle commence en `pending`, puis transitionne soit vers `fulfilled` (succès), soit vers `rejected` (échec). Cet état est ensuite final.

2. Créer et consommer une Promise

Pour bien comprendre le cycle de vie d'une Promise, il est essentiel de voir comment on la crée et comment on l'utilise.

La vidéo suivante, du cours de Grafikart.fr, est une excellente introduction pratique. Elle vous montrera comment créer une Promise, comment la résoudre ou la rejeter, et comment réagir à ces issues.

Apprendre le JavaScript : Promise

Cette vidéo vous guidera à travers la création et la consommation de Promises. Vous y découvrirez la syntaxe de base et les méthodes essentielles.

Regardez la vidéo du début jusqu'à 07:23. Concentrez-vous sur les points suivants : La syntaxe new Promise((resolve, reject) => { ... }). Le rôle des fonctions resolve et reject. L'utilisation des méthodes .then() pour le succès, .catch() pour l'échec, et .finally() pour le nettoyage.

Synthèse des concepts clés

Comme vous l'avez vu, il y a deux côtés à une Promise : le créateur (la fonction qui retourne la Promise) et le consommateur (le code qui l'utilise).

Créer une Promise :
On utilise le constructeur new Promise. Il prend une fonction (appelée "executor") avec deux arguments : resolve et reject.

  • Appelez resolve(valeur) lorsque l'opération réussit.
  • Appelez reject(erreur) lorsque l'opération échoue.
function fetchUserData() {
  return new Promise((resolve, reject) => {
    // Simule un appel réseau qui prend 1 seconde
    setTimeout(() => {
      const success = true; // Changez à 'false' pour tester l'échec
      if (success) {
        resolve({ id: 1, name: "John Doe" });
      } else {
        reject(new Error("Impossible de récupérer les données utilisateur."));
      }
    }, 1000);
  });
}

Consommer une Promise :
Une fois que vous avez une Promise, vous utilisez des méthodes pour "attendre" son résultat.

  • .then(onFulfilled, onRejected) : La méthode principale.
    • onFulfilled est appelée si la Promise est fulfilled. Elle reçoit la valeur passée à resolve.
    • onRejected est appelée si la Promise est rejected. Elle reçoit l'erreur passée à reject.
  • .catch(onRejected) : C'est un raccourci syntaxique pour .then(null, onRejected). C'est la manière la plus courante et la plus lisible de gérer les erreurs.
  • .finally(onFinally) : La fonction onFinally est exécutée que la Promise soit tenue ou rejetée. Idéal pour le code de nettoyage (ex: cacher un indicateur de chargement).
const userPromise = fetchUserData();

userPromise
  .then(user => {
    console.log("Succès :", user.name);
  })
  .catch(error => {
    console.error("Erreur :", error.message);
  })
  .finally(() => {
    console.log("Opération terminée.");
  });

3. La puissance du chaînage (Chaining)

Le véritable avantage des Promises par rapport aux callbacks est leur capacité à être chaînées.

Chaque appel à .then(), .catch() ou .finally() retourne une nouvelle Promise. Cela permet de créer des séquences d'opérations asynchrones lisibles et plates, évitant ainsi le "pyramide de la mort" (pyramid of doom).

Promises - 33 JavaScript Concepts

L'article 'Promises - 33 JavaScript Concepts' contient une excellente section qui illustre le chaînage et la règle la plus importante à respecter.

Lisez la section intitulée 'Promise Chaining'. Portez une attention particulière à la sous-section 'The #1 Rule of Chaining: Always return from your .then() callbacks!'. C'est l'erreur la plus commune chez les débutants.

Comment fonctionne le chaînage ?

La valeur que vous retournez dans un .then() devient la valeur de résolution de la Promise suivante dans la chaîne.

Promise.resolve(2) // Crée une Promise déjà résolue avec la valeur 2
  .then(value => {
    console.log(value); // 2
    return value * 2;   // retourne 4
  })
  .then(value => {
    console.log(value); // 4
    // Si on retourne une nouvelle Promise, la chaîne attend qu'elle se résolve
    return new Promise(resolve => setTimeout(() => resolve(value + 10), 500)); 
  })
  .then(value => {
    console.log(value); // 14 (après 500ms d'attente)
  });

La règle d'or : TOUJOURS return dans un .then() si vous voulez passer une valeur (ou une autre Promise) à l'étape suivante. Si vous oubliez, l'étape suivante recevra undefined.


4. Gérer plusieurs Promises simultanément

Que faire si vous devez lancer plusieurs opérations asynchrones en parallèle et attendre qu'elles soient toutes terminées ? C'est un cas très courant (ex: charger les informations d'un utilisateur et ses posts en même temps). Pour cela, la classe Promise fournit des méthodes statiques très utiles.

La suite de la vidéo de Grafikart explique parfaitement ces méthodes.

Apprendre le JavaScript : Promise

Cette partie de la vidéo aborde les méthodes statiques pour combiner des Promises, comme Promise.all.

Regardez la vidéo de 15:40 à 20:54. Concentrez-vous sur la différence entre Promise.all et Promise.allSettled, les deux méthodes les plus utiles au quotidien.

Voici un tableau récapitulatif basé sur la documentation :

Méthode Se résout quand... Rejette quand... Cas d'usage principal
Promise.all() TOUTES les Promises se résolvent. UNE SEULE Promise rejette (échoue immédiatement). Vous avez besoin de tous les résultats pour continuer. C'est un "tout ou rien".
Promise.allSettled() TOUTES les Promises sont réglées (résolues ou rejetées). Jamais. Vous voulez tenter toutes les opérations et savoir lesquelles ont réussi ou échoué.
Promise.race() La première Promise qui se règle (résout ou rejette). La première Promise qui se règle rejette. Implémenter un timeout pour une opération.
Promise.any() La première Promise qui se résout. TOUTES les Promises rejettent. Tenter plusieurs sources pour une même ressource et prendre la première qui répond.

Exemple avec Promise.all:

const promiseUser = fetchUserData();
const promisePosts = fetchUserPosts(); // Supposons que cette fonction existe

Promise.all([promiseUser, promisePosts])
  .then(([user, posts]) => {
    // Ce bloc s'exécute seulement si les DEUX promises réussissent
    console.log(`Utilisateur: ${user.name}`);
    console.log(`Nombre de posts: ${posts.length}`);
  })
  .catch(error => {
    // Ce bloc s'exécute si L'UNE des promises échoue
    console.error("Une des opérations a échoué:", error);
  });

5. Lien avec la Boucle d'Événements

Rappelez-vous de la leçon précédente : nous avons distingué la Task Queue de la Microtask Queue. C'est ici que cette connaissance devient cruciale.

Les callbacks des Promises (.then, .catch, .finally) sont ajoutés à la Microtask Queue. Cette file a une priorité plus élevée : elle est entièrement vidée après chaque tâche de la Task Queue.

Regardons un dernier extrait vidéo pour solidifier ce concept.

Apprendre le JavaScript : Promise

Cet extrait final montre clairement comment le code d'une Promise interagit avec l'Event Loop, en confirmant que les callbacks .then sont asynchrones et passent par la file d'attente.

Regardez la vidéo de 20:59 à 24:01. L'exemple illustre parfaitement pourquoi un .then() ne s'exécute pas immédiatement, même si la Promise est déjà résolue. C'est la boucle d'événements en action.

L'ordre d'exécution est donc :

  1. Tout le code synchrone du script principal.
  2. Toutes les tâches dans la Microtask Queue (callbacks de Promises).
  3. Une tâche de la Task Queue (ex: un callback de setTimeout).
console.log("1. Début du script");

setTimeout(() => {
  console.log("4. setTimeout (Task Queue)");
}, 0);

Promise.resolve().then(() => {
  console.log("3. Promise.then (Microtask Queue)");
});

console.log("2. Fin du script");

// Ordre d'affichage dans la console :
// 1. Début du script
// 2. Fin du script
// 3. Promise.then (Microtask Queue)
// 4. setTimeout (Task Queue)

Conclusion

Aujourd'hui, vous avez appris à manier les Promises, la pierre angulaire de la programmation asynchrone en JavaScript moderne.

Points clés à retenir :

  • Une Promise est un objet représentant le résultat futur d'une opération asynchrone, avec trois états : pending, fulfilled, et rejected.
  • On consomme une Promise avec .then() pour le succès, .catch() pour l'échec, et .finally() pour le nettoyage.
  • Le chaînage est la force des Promises, permettant d'écrire du code asynchrone plat et lisible. N'oubliez jamais de return la valeur ou la Promise suivante.
  • Promise.all() et Promise.allSettled() sont des outils puissants pour gérer des opérations concurrentes.
  • Les callbacks de Promises utilisent la Microtask Queue, ce qui leur donne une haute priorité d'exécution dans la boucle d'événements.

Dans la prochaine leçon, nous allons découvrir la syntaxe async/await. C'est une manière encore plus élégante et intuitive de travailler avec les Promises, qui vous permettra d'écrire du code asynchrone qui ressemble à du code synchrone.