
Chaînage de promesses et gestion des erreurs avec .then() et .catch()
Approfondissez l'utilisation des Promesses en Node.js : maitrisez le chainage avec .then() pour les operations sequentielles et la gestion d'erreurs robuste avec .catch().
Le pouvoir de l'enchaînement : La linéarité retrouvée
La caractéristique la plus puissante des promesses, qui résout directement le problème du "Callback Hell", est leur capacité à être enchaînées (chained). Comme nous l'avons vu, chaque appel à `.then()` (et aussi à `.catch()` et `.finally()`) retourne une nouvelle promesse. C'est ce mécanisme qui permet de lier des opérations asynchrones successives de manière linéaire et lisible.
Le comportement de la promesse retournée par `.then(onFulfilled, onRejected)` dépend de ce qui se passe à l'intérieur des fonctions `onFulfilled` ou `onRejected` :
- Si `onFulfilled` (ou `onRejected`) retourne une valeur (non-promesse), la nouvelle promesse retournée par `.then()` sera immédiatement tenue (fulfilled) avec cette valeur.
- Si `onFulfilled` (ou `onRejected`) lance une erreur (`throw error`), la nouvelle promesse sera immédiatement rejetée (rejected) avec cette erreur.
- Si `onFulfilled` (ou `onRejected`) retourne une autre promesse (promesse B), la nouvelle promesse retournée par `.then()` (promesse A) va "adopter" l'état de la promesse B. C'est-à-dire que la promesse A attendra que la promesse B se termine, puis elle sera tenue ou rejetée avec la même valeur ou raison que la promesse B. C'est le mécanisme clé pour enchaîner des opérations asynchrones.
Mise en pratique du chaînage avec `.then()`
Voyons comment cela fonctionne concrètement pour exécuter plusieurs étapes asynchrones séquentiellement. Chaque étape est représentée par une fonction retournant une promesse.
// Fonction simulant une étape asynchrone
function etape(numero, delai, valeurPrecedente) {
return new Promise((resolve) => {
console.log(`Début étape ${numero} (reçu: ${valeurPrecedente})`);
setTimeout(() => {
const resultat = `Résultat étape ${numero} (depuis ${valeurPrecedente})`;
console.log(`Fin étape ${numero}`);
resolve(resultat);
}, delai);
});
}
console.log("Lancement de la séquence...");
etape(1, 1000, "Init") // Première étape
.then((res1) => { // Callback pour le succès de l'étape 1
console.log(" -> .then après étape 1");
// Utilise res1 et retourne la promesse pour l'étape 2
return etape(2, 500, res1);
})
.then((res2) => { // Callback pour le succès de l'étape 2
console.log(" -> .then après étape 2");
// Utilise res2 et retourne la promesse pour l'étape 3
return etape(3, 800, res2);
})
.then((res3) => { // Callback pour le succès de l'étape 3
console.log(" -> .then après étape 3");
console.log("Séquence terminée avec succès ! Résultat final:", res3);
});
console.log("Séquence initiée, attente des étapes...");
// Output attendu (dans l'ordre, avec les délais):
// Lancement de la séquence...
// Séquence initiée, attente des étapes...
// Début étape 1 (reçu: Init)
// (1s plus tard)
// Fin étape 1
// -> .then après étape 1
// Début étape 2 (reçu: Résultat étape 1 (depuis Init))
// (0.5s plus tard)
// Fin étape 2
// -> .then après étape 2
// Début étape 3 (reçu: Résultat étape 2 (depuis Résultat étape 1 (depuis Init)))
// (0.8s plus tard)
// Fin étape 3
// -> .then après étape 3
// Séquence terminée avec succès ! Résultat final: Résultat étape 3 (depuis Résultat étape 2 (depuis Résultat étape 1 (depuis Init)))Cette structure est beaucoup plus facile à suivre que des callbacks imbriqués. Chaque `.then` représente une étape séquentielle, et la valeur de résolution de l'étape précédente est passée à la suivante.
Gestion centralisée des erreurs avec `.catch()`
L'un des grands avantages du chaînage de promesses est la façon dont les erreurs sont propagées. Si une promesse dans la chaîne est rejetée (soit parce que l'opération asynchrone a échoué et a appelé `reject()`, soit parce qu'une erreur a été lancée dans un callback `.then()`), la chaîne va "court-circuiter" les callbacks `onFulfilled` des `.then()` suivants et chercher le premier gestionnaire d'erreur disponible, c'est-à-dire le premier callback `onRejected` (soit dans un `.catch()`, soit comme deuxième argument d'un `.then()`).
Cela permet de placer un unique bloc `.catch()` à la fin de la chaîne pour gérer toutes les erreurs potentielles survenues dans n'importe laquelle des étapes précédentes.
function operationQuiPeutEchouer(succesGaranti) {
return new Promise((resolve, reject) => {
console.log(`Lancement de l'opération (succès garanti: ${succesGaranti})`);
setTimeout(() => {
if (succesGaranti) {
console.log("Opération réussie.");
resolve("OK");
} else {
console.error("Opération échouée !");
reject(new Error("Echec volontaire de l'opération"));
}
}, 500);
});
}
console.log("Test avec échec possible...");
operationQuiPeutEchouer(true) // Première op réussit
.then(res1 => {
console.log("Premier .then, reçu:", res1);
return operationQuiPeutEchouer(false); // Seconde op va échouer
})
.then(res2 => {
// CE BLOC SERA IGNORE car la promesse précédente sera rejetée
console.log("Deuxième .then, reçu:", res2);
return "Jamais atteint";
})
.catch(erreur => {
// Ce bloc attrape l'erreur de la seconde opération
console.error("### Erreur attrapée dans le .catch() ! ###");
console.error("Message:", erreur.message);
// On peut arrêter ici, ou choisir de "récupérer" (voir section suivante)
});
// Output attendu:
// Test avec échec possible...
// Lancement de l'opération (succès garanti: true)
// (0.5s plus tard)
// Opération réussie.
// Premier .then, reçu: OK
// Lancement de l'opération (succès garanti: false)
// (0.5s plus tard)
// Opération échouée !
// ### Erreur attrapée dans le .catch() ! ###
// Message: Echec volontaire de l'opérationLe `.catch()` à la fin agit comme un filet de sécurité global pour la chaîne. C'est une amélioration majeure par rapport à la gestion d'erreurs répétitive et dispersée des callbacks imbriqués.
Récupération après une erreur
Un bloc `.catch()` ne signifie pas nécessairement la fin de la chaîne. Tout comme `.then()`, `.catch()` retourne aussi une nouvelle promesse. Si le callback `onRejected` dans `.catch()` :
- Retourne une valeur (non-promesse), la promesse retournée par `.catch()` sera tenue (fulfilled) avec cette valeur. Cela permet de gérer une erreur et de continuer la chaîne avec une valeur par défaut ou un état de récupération.
- Lance une erreur ou retourne une promesse rejetée, la promesse retournée par `.catch()` sera rejetée, propageant l'erreur (ou une nouvelle erreur) aux `.catch()` suivants.
Exemple de récupération :
operationQuiPeutEchouer(false) // L'opération échoue
.catch(erreur => {
console.error("Erreur attrapée:", erreur.message);
console.log("Tentative de récupération...");
// Retourne une valeur par défaut pour continuer
return "Donnée de secours";
})
.then(resultat => {
// Ce .then s'exécute car le .catch précédent a retourné une valeur
console.log("Suite après récupération, résultat:", resultat);
})
.catch(errFinale => {
// Ce catch ne serait atteint que si le .then après récupération lançait une erreur
console.error("Erreur finale inattendue:", errFinale);
});
// Output attendu:
// Lancement de l'opération (succès garanti: false)
// (0.5s plus tard)
// Opération échouée !
// Erreur attrapée: Echec volontaire de l'opération
// Tentative de récupération...
// Suite après récupération, résultat: Donnée de secoursCette capacité de récupération sélective est très utile pour construire des flux logiques résilients.
.catch() vs .then(null, onRejected)
Techniquement, vous pouvez gérer les erreurs en passant une deuxième fonction à `.then()` :
promesse.then(
resultat => { /* gestion succès */ },
erreur => { /* gestion erreur */ }
);Cependant, l'utilisation de `.catch()` est généralement préférée et considérée comme une meilleure pratique pour plusieurs raisons :
- Séparation des préoccupations : Elle sépare clairement le code de gestion du succès (dans `.then()`) du code de gestion de l'échec (dans `.catch()`), améliorant la lisibilité.
- Gestion des erreurs du code de succès : Un `.catch()` placé après un `.then()` attrapera non seulement les rejets de la promesse initiale, mais aussi les erreurs qui pourraient être lancées à l'intérieur du callback `onFulfilled` du `.then()` lui-même. Ce n'est pas le cas si vous utilisez uniquement le deuxième argument de `.then()`.
Promise.resolve("OK")
.then(resultat => {
console.log("Succès reçu:", resultat);
throw new Error("Erreur dans le callback de succès !");
}, erreurInitiale => {
// Ce gestionnaire ne sera pas appelé pour l'erreur lancée ci-dessus
console.error("Gestionnaire d'erreur du .then", erreurInitiale);
})
.catch(erreur => {
// CE CATCH attrapera l'erreur lancée dans le .then précédent
console.error("Erreur attrapée par .catch():", erreur.message);
});
// Output:
// Succès reçu: OK
// Erreur attrapée par .catch(): Erreur dans le callback de succès !Pour ces raisons, privilégiez l'utilisation de `.then(onFulfilled)` pour le chemin heureux et de `.catch(onRejected)` pour la gestion centralisée des erreurs dans vos chaînes de promesses.