
Async/await : simplifier l'écriture du code asynchrone
Maitrisez async/await en Node.js pour ecrire du code asynchrone clair et lisible, basé sur les Promesses, avec une gestion d'erreurs intuitive via try/catch.
La révolution `async/await` : L'asynchrone avec une syntaxe synchrone
Bien que les Promesses représentent une amélioration significative par rapport aux callbacks pour gérer l'asynchronisme, l'enchaînement de multiples `.then()` peut encore, dans certains cas complexes, rendre le code un peu verbeux. Pour simplifier davantage l'écriture et la lecture du code asynchrone, ECMAScript 2017 (ES8) a introduit une nouvelle syntaxe : `async/await`.
Il est fondamental de comprendre que `async/await` n'est pas un nouveau mécanisme d'asynchronisme fondamentalement différent. C'est du "sucre syntaxique" construit par-dessus les Promesses. Il fournit une manière d'écrire du code qui utilise des promesses mais qui ressemble et se lit beaucoup plus comme du code synchrone traditionnel, tout en conservant le comportement non bloquant de Node.js.
Cette syntaxe a révolutionné la manière dont les développeurs écrivent du code asynchrone en JavaScript et Node.js, le rendant plus intuitif, plus facile à déboguer (dans de nombreux cas) et plus simple à raisonner, en particulier pour les développeurs venant d'autres langages avec des modèles asynchrones différents.
Le mot-clé `async` : Déclarer des fonctions asynchrones
Le mot-clé `async` est utilisé pour déclarer une fonction comme étant asynchrone. Vous pouvez le placer avant une déclaration de fonction classique, une expression de fonction ou une fonction fléchée.
// Déclaration de fonction async
async function fetchData() {
// ... code utilisant await
}
// Expression de fonction async
const processData = async function() {
// ...
};
// Fonction fléchée async
const getUser = async (id) => {
// ...
};La principale conséquence de la déclaration d'une fonction avec `async` est qu'elle retourne implicitement une Promesse. Peu importe ce que la fonction retourne explicitement :
- Si la fonction `async` retourne une valeur (non-promesse), la promesse retournée par la fonction `async` sera tenue (fulfilled) avec cette valeur.
- Si la fonction `async` lance une erreur (`throw error`), la promesse retournée sera rejetée (rejected) avec cette erreur.
- Si la fonction `async` retourne explicitement une promesse, la promesse retournée par la fonction `async` adoptera simplement l'état de cette promesse retournée.
async function obtenirValeur() {
return 42; // Retourne une valeur
}
async function lancerErreur() {
throw new Error("Quelque chose a mal tourné");
}
async function retournerPromesse() {
return Promise.resolve("Promesse explicite");
}
obtenirValeur().then(val => console.log(val)); // Affiche 42
lancerErreur().catch(err => console.error(err.message)); // Affiche "Quelque chose a mal tourné"
retournerPromesse().then(val => console.log(val)); // Affiche "Promesse explicite"Le mot-clé `await` : Mettre en pause et attendre les promesses
Le mot-clé `await` est le véritable coeur de cette syntaxe. Il ne peut être utilisé qu'à l'intérieur d'une fonction déclarée avec `async` (ou au niveau supérieur dans les modules ES modernes de Node.js).
Lorsque vous placez `await` devant une expression qui évalue en une Promesse, il se passe plusieurs choses :
- L'exécution de la fonction `async` est mise en pause à cet endroit. Important : seule la fonction `async` est mise en pause, pas l'ensemble du programme ni la boucle d'événements. Node.js peut continuer à traiter d'autres événements pendant cette pause.
- L'opérateur `await` attend que la Promesse sur laquelle il est appliqué se termine (settle), c'est-à-dire qu'elle devienne soit tenue (fulfilled), soit rejetée (rejected).
- Une fois la Promesse terminée :
- Si elle est tenue, `await` retourne la valeur de résolution de la promesse. L'exécution de la fonction `async` reprend alors à partir de ce point.
- Si elle est rejetée, `await` lance (throws) la raison du rejet (l'objet d'erreur). Cette erreur peut alors être attrapée par un bloc `try...catch` environnant.
L'utilisation de `await` permet donc d'écrire du code qui attend le résultat d'une opération asynchrone (représentée par une promesse) de manière linéaire, comme si c'était un appel de fonction synchrone.
Reprenons l'exemple de l'enchaînement de promesses précédent avec `async/await` :
// Les fonctions etape1, etape2, etape3 retournent toujours des promesses
// ... (définitions de etape1, etape2, etape3 comme avant)
async function executerSequence(valeurInitiale) {
console.log("Début de la séquence async...");
try {
// Attend que etape1 se termine, obtient son résultat
const res1 = await etape1(valeurInitiale);
console.log(" -> Après await etape 1, reçu:", res1);
// Attend que etape2 se termine, obtient son résultat
const res2 = await etape2(res1);
console.log(" -> Après await etape 2, reçu:", res2);
// Attend que etape3 se termine, obtient son résultat
const res3 = await etape3(res2);
console.log(" -> Après await etape 3, reçu:", res3);
console.log("Séquence async terminée avec succès ! Résultat final:", res3);
return res3; // La promesse retournée par executerSequence sera tenue avec res3
} catch (erreur) {
console.error("### Erreur attrapée dans le bloc catch async ! ###");
console.error("Message:", erreur.message);
// La promesse retournée par executerSequence sera rejetée avec cette erreur
throw erreur;
}
}
console.log("Appel de la fonction async...");
executerSequence(5)
.then(final => console.log("Appelant a reçu le succès final:", final))
.catch(err => console.error("Appelant a reçu l'erreur finale:", err.message));
console.log("Appel async initié.");
// Essayer avec executerSequence(10) pour voir l'erreur attrapée par le catch asyncComparez la lisibilité de la fonction `executerSequence` avec l'enchaînement `.then()` précédent. Le flux logique est beaucoup plus direct et ressemble à du code synchrone.
Gestion des erreurs simplifiée avec `try...catch`
L'un des avantages majeurs de `async/await` est qu'il permet d'utiliser les blocs `try...catch` standards de JavaScript pour gérer les erreurs provenant des promesses rejetées. Puisque `await` lance une exception lorsqu'une promesse est rejetée, vous pouvez simplement entourer vos appels `await` (ou un groupe d'appels) d'un bloc `try...catch`.
async function exempleErreur() {
try {
console.log("Tentative d'opération...");
const resultat = await operationQuiPeutEchouer(false); // Attend une promesse qui va rejeter
// Cette ligne ne sera pas atteinte si l'opération échoue
console.log("Succès (ne devrait pas s'afficher) :", resultat);
} catch (erreur) {
// L'erreur lancée par await (provenant du reject de la promesse) est attrapée ici
console.error("Erreur attrapée par try/catch:", erreur.message);
}
console.log("Après le bloc try/catch.");
}
exempleErreur();
// Output attendu (si operationQuiPeutEchouer est définie comme avant) :
// Tentative d'opération...
// Lancement de l'opération (succès garanti: false)
// (0.5s plus tard)
// Opération échouée !
// Erreur attrapée par try/catch: Echec volontaire de l'opération
// Après le bloc try/catch.Cela rend la gestion des erreurs beaucoup plus naturelle et cohérente avec la gestion des erreurs synchrones. Vous pouvez avoir plusieurs blocs `try...catch` pour gérer différentes erreurs, ou un seul bloc global pour une fonction `async` entière.
Relation avec les promesses et points d'attention
Il est essentiel de se rappeler que `async/await` fonctionne avec les promesses, pas à leur place. Vous `await`ez des promesses, et les fonctions `async` retournent des promesses. Vous pouvez donc tout à fait mélanger les syntaxes :
async function exempleMixte() {
const promesse = operationQuiPeutEchouer(true);
// On peut toujours utiliser .then() sur la promesse retournée par l'appel
promesse.then(res => console.log(".then() externe sur la promesse: ", res));
// Et on peut await la même promesse à l'intérieur
try {
const resultatAwait = await promesse;
console.log("Résultat de await sur la même promesse:", resultatAwait);
} catch(e) { /* ... */ }
}
// Ou appeler une fonction async et utiliser .then()/.catch() sur son résultat
executerSequence(5)
.then(res => console.log("Succès final reçu par .then"))
.catch(err => console.log("Erreur finale reçue par .catch"));Un point d'attention important concerne le parallélisme. Si vous avez plusieurs opérations asynchrones indépendantes que vous souhaitez exécuter en parallèle pour gagner du temps, utiliser `await` séquentiellement pour chacune d'elles serait inefficace, car chaque `await` attendrait la fin de l'opération précédente avant de lancer la suivante.
async function sequentielInefficace() {
console.time("sequentiel");
const res1 = await etape(1, 1000, 'A'); // Attend 1s
const res2 = await etape(2, 1000, 'B'); // Attend 1s de plus
console.timeEnd("sequentiel"); // Total ~2s
return [res1, res2];
}
async function paralleleEfficace() {
console.time("parallele");
// Lance les deux opérations en même temps
const promesse1 = etape(1, 1000, 'A');
const promesse2 = etape(2, 1000, 'B');
// Attend que les deux promesses soient terminées en parallèle
const [res1, res2] = await Promise.all([promesse1, promesse2]);
console.timeEnd("parallele"); // Total ~1s
return [res1, res2];
}
// sequentielInefficace();
// paralleleEfficace();Dans ce cas, il faut utiliser des méthodes comme `Promise.all()` pour lancer les opérations en parallèle et attendre ensuite leur achèvement groupé avec un seul `await`.
En conclusion, `async/await` est un outil puissant qui simplifie considérablement l'écriture, la lecture et la maintenance du code asynchrone en Node.js en s'appuyant sur le système de Promesses. Il est devenu le standard de facto pour la gestion de l'asynchronisme dans la plupart des nouveaux projets Node.js.