
Les promesses (Promises) : une approche moderne de l'asynchronisme
Decouvrez les Promesses (Promises) ES6, une solution elegante au Callback Hell. Apprenez a creer, utiliser (.then, .catch) et enchainer les promesses en Node.js.
La solution au "Callback Hell" : L'objet Promise
Face aux problèmes de lisibilité et de maintenabilité posés par l'imbrication excessive des callbacks (le "Callback Hell"), ECMAScript 2015 (ES6) a introduit une nouvelle approche standardisée pour gérer les opérations asynchrones : les Promesses (Promises). Une promesse est un objet qui représente la valeur éventuelle (ou la raison de l'échec) d'une opération asynchrone qui n'est pas encore terminée.
Plutôt que de passer un callback qui sera exécuté plus tard, une fonction asynchrone qui utilise des promesses retourne immédiatement cet objet "promesse". Cet objet sert de substitut (placeholder) pour le résultat futur. La promesse possède des méthodes (`.then()`, `.catch()`) qui permettent d'enregistrer les fonctions à exécuter une fois que l'opération asynchrone sous-jacente sera terminée, que ce soit avec succès ou en erreur.
L'avantage principal des promesses est leur capacité à être enchaînées de manière linéaire et leur gestion des erreurs centralisée, rendant le code asynchrone beaucoup plus plat, lisible et facile à raisonner, se rapprochant d'une écriture séquentielle.
Le cycle de vie et la création d'une promesse
Une promesse peut se trouver dans l'un des trois états suivants :
- En attente (Pending) : L'état initial. L'opération asynchrone n'est pas encore terminée.
- Tenue/Accomplie (Fulfilled/Resolved) : L'opération asynchrone s'est terminée avec succès. La promesse détient maintenant une valeur résultante.
- Rejetée (Rejected) : L'opération asynchrone a échoué. La promesse détient maintenant une raison de l'échec (généralement un objet `Error`).
Une promesse passe de l'état `pending` à `fulfilled` ou `rejected` une seule fois. Une fois qu'elle est tenue ou rejetée, son état ne change plus jamais.
Bien que vous consommiez le plus souvent des promesses retournées par des API Node.js modernes ou des bibliothèques, il est utile de savoir comment en créer une, notamment pour "promisifier" d'anciennes API basées sur des callbacks. On utilise le constructeur `Promise` :
const maPromesse = new Promise((resolve, reject) => {
// Ici, on lance l'opération asynchrone (ex: setTimeout, requête DB, etc.)
console.log("Opération asynchrone démarrée...");
setTimeout(() => {
const succes = Math.random() > 0.3; // Simuler un succès ou un échec aléatoire
if (succes) {
// Si succès, appeler resolve() avec la valeur résultante
const resultat = { data: "Données récupérées avec succès !" };
console.log("Opération réussie, appel de resolve()...");
resolve(resultat);
} else {
// Si échec, appeler reject() avec la raison (souvent un objet Error)
const erreur = new Error("L'opération asynchrone a échoué.");
console.error("Opération échouée, appel de reject()...");
reject(erreur);
}
}, 2000);
});
console.log("Promesse créée, état: pending");
// Le code ici continue de s'exécuter immédiatement
// Comment utiliser cette promesse est vu dans la section suivante...Le constructeur `Promise` prend une fonction (appelée "executor") en argument. Cet executor reçoit lui-même deux fonctions en arguments : `resolve` et `reject`. C'est à l'intérieur de l'executor que vous lancez votre opération asynchrone. Lorsque celle-ci se termine, vous devez appeler `resolve(valeur)` en cas de succès, ou `reject(raison)` en cas d'échec.
Consommer des promesses : `.then()` et `.catch()`
Une fois que vous avez une promesse (soit créée par vous, soit retournée par une API), vous pouvez enregistrer des fonctions à exécuter lorsque la promesse change d'état en utilisant les méthodes `.then()` et `.catch()`.
La méthode `.then(onFulfilled, onRejected)` :
- Permet d'enregistrer jusqu'à deux callbacks :
- `onFulfilled` : Fonction exécutée si la promesse est tenue (fulfilled). Elle reçoit la valeur de résolution en argument.
- `onRejected` (Optionnel) : Fonction exécutée si la promesse est rejetée (rejected). Elle reçoit la raison du rejet (l'erreur) en argument.
- Très important : `.then()` retourne toujours une nouvelle promesse. Cela permet l'enchaînement.
- Si `onFulfilled` ou `onRejected` retourne une valeur, la nouvelle promesse retournée par `.then()` sera tenue avec cette valeur.
- Si `onFulfilled` ou `onRejected` lance une erreur (`throw`), la nouvelle promesse sera rejetée avec cette erreur.
- Si `onFulfilled` ou `onRejected` retourne une autre promesse, la nouvelle promesse retournée par `.then()` adoptera l'état et la valeur/raison de cette promesse retournée (c'est la clé de l'enchaînement).
La méthode `.catch(onRejected)` :
- C'est un raccourci syntaxique pour `.then(null, onRejected)`.
- Elle permet d'enregistrer un callback uniquement pour le cas où la promesse (ou une promesse précédente dans la chaîne) est rejetée.
- Elle retourne également une nouvelle promesse, ce qui permet de gérer une erreur puis de continuer la chaîne si nécessaire.
Exemple d'utilisation de la promesse créée précédemment :
console.log("Enregistrement des callbacks sur la promesse...");
maPromesse
.then((resultatSucces) => {
// Exécuté si resolve() a été appelé
console.log("Callback .then() exécuté (Succès):");
console.log(resultatSucces);
// On peut retourner une nouvelle valeur ou une nouvelle promesse ici
return "Traitement du succès terminé.";
})
.catch((erreur) => {
// Exécuté si reject() a été appelé (ou si une erreur a été lancée dans un .then précédent)
console.error("Callback .catch() exécuté (Echec):");
console.error(erreur.message);
// On peut choisir de gérer l'erreur et de "récupérer" en retournant une valeur
// return "Erreur gérée, valeur par défaut.";
// Ou de relancer l'erreur (ou une autre) pour qu'elle soit attrapée plus loin
throw new Error("Impossible de continuer après l'échec.");
})
.then((resultatFinal) => {
// Ce .then s'exécute si le .then précédent a réussi OU si le .catch a retourné une valeur (pas lancé d'erreur)
console.log("Deuxième .then() après succès ou récupération:", resultatFinal);
})
.catch((erreurFinale) => {
// Ce .catch attrape les erreurs du .then précédent ou du .catch précédent s'il a lancé une erreur
console.error("Dernier .catch():", erreurFinale.message);
});
console.log("Callbacks enregistrés.");
// Le output dépendra du succès ou de l'échec aléatoire dans la création de la promesse.Enchaînement de promesses (Chaining)
La véritable puissance des promesses réside dans leur capacité à être enchaînées. Puisque `.then()` retourne une nouvelle promesse, vous pouvez facilement lier séquentiellement plusieurs opérations asynchrones où chaque étape dépend du résultat de la précédente.
Imaginez trois fonctions (`etape1`, `etape2`, `etape3`) qui retournent chacune une promesse :
// Fonctions simulant des opérations asynchrones retournant des promesses
function etape1(valeurInitiale) {
return new Promise((resolve) => {
console.log("Etape 1 démarrée avec", valeurInitiale);
setTimeout(() => resolve(valeurInitiale * 2), 500);
});
}
function etape2(valeurEtape1) {
return new Promise((resolve) => {
console.log("Etape 2 démarrée avec", valeurEtape1);
setTimeout(() => resolve(valeurEtape1 + 10), 500);
});
}
function etape3(valeurEtape2) {
return new Promise((resolve, reject) => {
console.log("Etape 3 démarrée avec", valeurEtape2);
if (valeurEtape2 < 30) {
setTimeout(() => resolve(`Résultat final: ${valeurEtape2}`), 500);
} else {
setTimeout(() => reject(new Error("Valeur trop élevée pour l'étape 3")), 500);
}
});
}
// Enchaînement des promesses
console.log("Début de l'enchaînement...");
etape1(5) // Commence avec la valeur 5
.then(resultat1 => {
// resultat1 contient la valeur résolue de etape1 (5 * 2 = 10)
console.log("Etape 1 terminée, résultat:", resultat1);
return etape2(resultat1); // Retourne la promesse de etape2
})
.then(resultat2 => {
// resultat2 contient la valeur résolue de etape2 (10 + 10 = 20)
console.log("Etape 2 terminée, résultat:", resultat2);
return etape3(resultat2); // Retourne la promesse de etape3
})
.then(resultatFinal => {
// resultatFinal contient la valeur résolue de etape3 ("Résultat final: 20")
console.log("Etape 3 terminée avec succès:", resultatFinal);
})
.catch(erreur => {
// Ce catch unique attrape toute erreur survenue dans etape1, etape2, etape3
// ou dans les callbacks .then() précédents.
console.error("Une erreur est survenue dans la chaîne:", erreur.message);
});
console.log("Enchaînement initié.");
// Essayer avec etape1(10) pour voir le chemin d'erreur de etape3Ce code est beaucoup plus plat et lisible que l'équivalent avec des callbacks imbriqués. La gestion des erreurs est également centralisée : une seule `.catch()` à la fin de la chaîne peut attraper une erreur survenue à n'importe quelle étape précédente.
Les promesses fournissent donc une base solide et structurée pour gérer l'asynchronisme, améliorant la lisibilité, la composabilité et la gestion des erreurs par rapport aux callbacks traditionnels. Elles sont le fondement sur lequel repose la syntaxe `async/await` que nous verrons ensuite.