frontend / javascript
Async/await
Explication
Ce que vous allez apprendre
- Réécrire une chaîne de
.then()en fonctionasync/awaitéquivalente - Expliquer ce que
awaitsuspend réellement, et ce qui continue de s'exécuter pendant ce temps - Capturer les erreurs d'une fonction
asyncavectry/catch - Détecter une perte de parallélisme causée par des
awaitséquentiels inutiles - Corriger ce cas avec
Promise.allquand les opérations sont indépendantes
Dans quel contexte ?
Une page de profil met 4 secondes à s'afficher alors que chaque appel API individuel ne prend que 1 seconde. Le code contient const user = await getUser(id); const posts = await getPosts(id); const followers = await getFollowers(id); const stats = await getStats(id); : quatre requêtes indépendantes, exécutées l'une après l'autre au lieu d'en parallèle. Remplacer ces quatre lignes par const [user, posts, followers, stats] = await Promise.all([getUser(id), getPosts(id), getFollowers(id), getStats(id)]); fait chuter le temps de chargement à environ 1 seconde.
1. Un problème de lisibilité, pas de fonctionnement
Les Promises ont résolu le problème d'imbrication des callbacks, mais leur syntaxe en chaîne de .then() reste parfois difficile à suivre, surtout avec des conditions ou des boucles. async/await ne change rien au fonctionnement sous-jacent : c'est du "sucre syntaxique" au-dessus des Promises.
Prérequis
Cette leçon suppose que les trois états d'une Promise (pending, fulfilled, rejected) et le chaînage .then()/.catch() de la leçon précédente sont bien compris : async/await s'appuie exactement sur le même mécanisme, avec une autre syntaxe.
2. Écrire de l'asynchrone qui se lit comme du synchrone
Ce sucre syntaxique permet d'écrire du code asynchrone qui se LIT comme du code synchrone, ligne après ligne, ce qui réduit considérablement la charge mentale par rapport à une chaîne de .then().
3. Ce que fait vraiment await
Le mot-clé await suspend l'exécution de la fonction à cet endroit précis, jusqu'à ce que la Promise concernée se résolve, puis reprend avec le résultat directement disponible. Contrairement à ce que le mot "attendre" pourrait laisser penser, seule la fonction courante est mise en pause : le reste du programme continue de tourner normalement.
4. async retourne toujours une Promise
Une fonction déclarée async retourne toujours une Promise, même sans await explicite. Une erreur levée à l'intérieur se transforme automatiquement en rejet, capturable avec un try/catch classique — plus naturel qu'une chaîne de .catch().
| Style | Gestion des erreurs | Lisibilité en séquence |
|---|---|---|
.then()/.catch() | Un .catch() par chaîne | Peut devenir dense avec des conditions |
async/await | try/catch classique | Se lit comme du code synchrone |
5. Le piège le plus fréquent de cette syntaxe
Écrire await deux fois de suite pour des opérations indépendantes les exécute en séquence, doublant le temps total, alors qu'un Promise.all les lancerait en même temps. C'est une perte de parallélisme facile à ne pas remarquer.
Piège fréquent
const a = await taskA(); const b = await taskB(); exécute taskB seulement après la fin de taskA, même si elles n'ont aucun lien. Si les deux sont indépendantes, lancez-les d'abord (const pA = taskA(); const pB = taskB();) puis attendez-les ensemble (const [a, b] = await Promise.all([pA, pB]);).
6. La bonne question à se poser
Il faut donc distinguer les cas où l'attente séquentielle est réellement nécessaire (chaque étape dépend du résultat de la précédente) de ceux où elle ne l'est pas — et dans ce second cas, préférer systématiquement Promise.all.
Commandes & code
Async/await
La syntaxe qui rend le code asynchrone aussi lisible que du code synchrone.
function delai(ms, valeur) {
return new Promise((resolve) => setTimeout(() => resolve(valeur), ms));
}
// async transforme une fonction pour qu'elle retourne TOUJOURS une Promise
async function chargerUtilisateur(id) {
if (id <= 0) throw new Error("ID invalide"); // equivalent a reject()
const donnees = await delai(300, { id, nom: `User${id}` }); // suspend jusqu'a resolution
return donnees; // equivalent a resolve(donnees)
}
chargerUtilisateur(1).then(console.log); // async function reste utilisable avec .then()
// --- await : uniquement utilisable dans une fonction async (ou au top-level des modules ES) ---
async function main() {
console.log("Debut");
const utilisateur = await chargerUtilisateur(1); // le code suivant ATTEND la resolution
console.log("Utilisateur recu :", utilisateur);
console.log("Fin");
}
main();
// --- Gestion d'erreur : try/catch remplace .catch() ---
async function chargerAvecGestionErreur(id) {
try {
const utilisateur = await chargerUtilisateur(id);
return utilisateur;
} catch (erreur) {
console.error("Erreur capturee :", erreur.message);
return null;
} finally {
console.log("Nettoyage, dans tous les cas");
}
}
chargerAvecGestionErreur(-1);
// --- PIEGE : await sequentiel involontaire (perte de parallelisme) ---
async function sequentielLent() {
const debut = Date.now();
const a = await delai(500, "A"); // attend 500ms...
const b = await delai(500, "B"); // ...PUIS attend encore 500ms -- total 1000ms
console.log(`Sequentiel : ${Date.now() - debut}ms`);
return [a, b];
}
// --- Correction : lancer en parallele avec Promise.all ---
async function parallele() {
const debut = Date.now();
const [a, b] = await Promise.all([delai(500, "A"), delai(500, "B")]); // total ~500ms
console.log(`Parallele : ${Date.now() - debut}ms`);
return [a, b];
}
// --- await dans une boucle : sequentiel par nature (souvent voulu, parfois un piege) ---
async function traiterSequentiellement(ids) {
const resultats = [];
for (const id of ids) {
resultats.push(await chargerUtilisateur(id)); // un a la fois, dans l'ordre
}
return resultats;
}
async function traiterEnParallele(ids) {
// toutes les requetes partent EN MEME TEMPS
return Promise.all(ids.map((id) => chargerUtilisateur(id)));
}
// --- for await...of : consommer des Promises OU des async generators ---
async function* flux() {
yield delai(100, "a");
yield delai(100, "b");
yield delai(100, "c");
}
async function consommerFlux() {
for await (const valeur of flux()) {
console.log("Recu :", valeur);
}
}
consommerFlux();
// --- Top-level await (ES2022, dans les modules ES uniquement) ---
// fichier module.mjs :
// const config = await fetch("/config.json").then((r) => r.json());
// export default config;
// --- Combiner async/await avec allSettled pour ne jamais interrompre le traitement ---
async function chargerTousLesUtilisateurs(ids) {
const resultats = await Promise.allSettled(ids.map(chargerUtilisateur));
return resultats
.filter((r) => r.status === "fulfilled")
.map((r) => r.value);
}Résumé
async/awaitest du sucre syntaxique au-dessus des Promises : lisibilité, pas performance.- Une fonction
asyncretourne toujours une Promise ;try/catchremplace.catch(). awaitséquentiel dans du code indépendant fait perdre le parallélisme : préférerPromise.all.for await...ofconsomme aussi bien un tableau de Promises qu'un générateur asynchrone.
Exercices pratiques
Mission : la page de profil qui met 4 secondes à charger
Objectif : Diagnostiquer une perte de parallélisme causée par des await séquentiels inutiles, et corriger avec Promise.all sans casser l'ordre de dépendance réelle.
Contexte
La page de profil de Technologik met 4 secondes à s'afficher alors que chaque appel API individuel prend 1 seconde. Le code :
async function chargerProfil(id) {
const user = await getUser(id);
const posts = await getPosts(id);
const followers = await getFollowers(id);
const stats = await getStats(id);
return { user, posts, followers, stats };
}Aucune de ces quatre requêtes ne dépend du résultat d'une autre : elles pourraient toutes partir en même temps. Tu dois corriger la fonction pour qu'elle charge tout en parallèle, sans changer la forme de l'objet retourné.