6.2 — Asynchronisme
Débutant
🎯 Objectif : maîtriser Promises et
async/awaitau point que tu lis du code asynchrone aussi facilement que du synchrone.
À l'issue de cet axe, tu sauras :
- Décrire l'event loop, microtasks vs macrotasks
- Créer et chaîner des Promises
- Utiliser async/await avec gestion d'erreur propre
- Connaître Promise.all, allSettled, race, any
- Annuler une requête fetch avec AbortController
Pourquoi async ?
Section intitulée « Pourquoi async ? »JavaScript dans le navigateur (et Node) est mono-thread. Si tu attends une réponse réseau pendant 500 ms en bloquant, rien d’autre ne se passe : pas de scroll, pas de clic, UI gelée.
L’asynchrone permet de dire “fais-le et préviens-moi” sans bloquer le thread.
L’event loop — modèle mental
Section intitulée « L’event loop — modèle mental »flowchart TD
Start[Start] --> Sync[Pile d'appel<br/>code synchrone]
Sync -->|terminé| Micro[Vider toutes les microtasks<br/>then de Promises, queueMicrotask]
Micro --> Render{Rendu nécessaire ?}
Render -->|oui| Paint[requestAnimationFrame<br/>+ paint]
Render -->|non| Macro[Prendre 1 macrotask<br/>setTimeout, événement, fetch...]
Paint --> Macro
Macro --> Sync Règle d’exécution :
- Le code synchrone tourne jusqu’à la fin de la pile d’appel.
- Toutes les microtasks sont exécutées (Promises résolues,
queueMicrotask). - Une macrotask est traitée (setTimeout callback, événement DOM, retour de
fetch…). - Retour à l’étape 1.
console.log('1');setTimeout(() => console.log('2'), 0);Promise.resolve().then(() => console.log('3'));console.log('4');// 1, 4, 3, 21et4: synchrone immédiat.3: microtask, vidée avant la prochaine macrotask.2: macrotask, au tour suivant.
Callbacks — l’ancien
Section intitulée « Callbacks — l’ancien »fs.readFile('data.json', 'utf-8', (err, data) => { if (err) return console.error(err); parseJson(data, (err, json) => { if (err) return console.error(err); saveDb(json, (err) => { // callback hell — pyramide de la mort if (err) return console.error(err); console.log('OK'); }); });});C’est la raison pour laquelle Promises ont été inventées.
Promises — la base
Section intitulée « Promises — la base »Une Promise = un objet qui représente une opération asynchrone et son futur résultat. 3 états :
pending: en coursfulfilled: succès, avec une valeurrejected: échec, avec une raison
Créer une Promise
Section intitulée « Créer une Promise »const p = new Promise((resolve, reject) => { setTimeout(() => { if (Math.random() > 0.5) resolve('OK'); else reject(new Error('Échec')); }, 1000);});En vrai, tu créeras rarement une Promise toi-même. Tu en consommes : fetch(), fs.promises.readFile, etc. retournent déjà des Promises.
Consommer
Section intitulée « Consommer »fetch('/api/users') .then(response => response.json()) .then(users => console.log(users)) .catch(err => console.error(err)) .finally(() => console.log('Terminé'));async/await — le confort moderne
Section intitulée « async/await — le confort moderne »async function loadUsers() { try { const response = await fetch('/api/users'); if (!response.ok) throw new Error(`HTTP ${response.status}`); const users = await response.json(); return users; } catch (err) { console.error(err); return []; }}async : la fonction renvoie automatiquement une Promise.
await : pause l’exécution de la fonction jusqu’à ce que la Promise se résolve.
Top-level await
Section intitulée « Top-level await »// Fichier ESM (.mjs ou "type": "module")const data = await fetch('/api/data').then(r => r.json());console.log(data);Marche dans Node, dans Vite, dans Astro… Pas dans un module CommonJS classique.
Patterns courants
Section intitulée « Patterns courants »Paralléliser — Promise.all
Section intitulée « Paralléliser — Promise.all »// ❌ Sériel : 3 secondes au total si chaque requête en prend 1const a = await fetch('/a');const b = await fetch('/b');const c = await fetch('/c');
// ✅ Parallèle : 1 seconde au totalconst [a, b, c] = await Promise.all([ fetch('/a'), fetch('/b'), fetch('/c'),]);Promise.all rejette si n’importe laquelle rejette. Si tu veux savoir le résultat de chacune même en cas d’échec partiel : Promise.allSettled.
const results = await Promise.allSettled([ fetch('/a'), fetch('/b'),]);
results.forEach(r => { if (r.status === 'fulfilled') console.log(r.value); else console.error(r.reason);});Course — Promise.race et Promise.any
Section intitulée « Course — Promise.race et Promise.any »// race : la première qui se résout (ou rejette)const result = await Promise.race([ fetch('/api'), new Promise((_, rej) => setTimeout(() => rej('timeout'), 5000)),]);
// any : la première qui SUCCÈS (rejette si toutes échouent)const fastest = await Promise.any([ fetch('https://eu.api/data'), fetch('https://us.api/data'), fetch('https://asia.api/data'),]);Séquentiel mais propre — boucle async
Section intitulée « Séquentiel mais propre — boucle async »async function loadAllUsers(ids) { const results = []; for (const id of ids) { results.push(await fetchUser(id)); // séquentiel } return results;}Attention : forEach n’attend pas. Pour itérer en async, utilise for…of.
// ❌ Ne fonctionne pas comme prévuids.forEach(async (id) => { await fetchUser(id); // démarre tout, on ne sait pas quand ça finit});
// ✅ Séquentielfor (const id of ids) { await fetchUser(id);}
// ✅ Parallèleawait Promise.all(ids.map(id => fetchUser(id)));Piège réel rencontré — `await` dans `forEach` ne fait rien Tests
🩹 Symptôme :
items.forEach(async (item) => { await db.execute({ sql: 'INSERT ...', args: [item] });});console.log('done'); // s'affiche AVANT la fin des inserts🔍 Cause : Array.prototype.forEach ignore la valeur de retour du callback. Le async crée une Promise qui n’est pas attendue. Le code continue immédiatement.
🩺 Fix : utiliser for...of (séquentiel) ou Promise.all(.map(...)) (parallèle).
// ✅ Séquentielfor (const item of items) { await db.execute({ sql: 'INSERT ...', args: [item] });}
// ✅ Parallèleawait Promise.all(items.map(item => db.execute(...)));🧠 Leçon : forEach n’est jamais le bon choix avec async. Si tu vois forEach(async ...) dans une PR, c’est presque toujours un bug. Linter ESLint : règle no-misleading-character-class détecte certains cas.
Gestion d’erreur
Section intitulée « Gestion d’erreur »try/catch autour d’await
Section intitulée « try/catch autour d’await »async function load() { try { const data = await fetch('/api'); return data; } catch (err) { if (err instanceof TypeError) { // erreur réseau } throw err; // propager si on ne peut pas gérer }}Erreurs non capturées
Section intitulée « Erreurs non capturées »window.addEventListener('unhandledrejection', (event) => { console.error('Promise non capturée :', event.reason);});Dans Node :
process.on('unhandledRejection', (reason) => { console.error('Promise non capturée :', reason);});Pattern “Result” pour éviter les try/catch
Section intitulée « Pattern “Result” pour éviter les try/catch »async function safeRequest(url) { try { const r = await fetch(url); return { ok: true, data: await r.json() }; } catch (err) { return { ok: false, error: err }; }}
const result = await safeRequest('/api');if (result.ok) { console.log(result.data);} else { console.error(result.error);}Ressemble à Result<T, E> en Rust. Pratique quand l’erreur est “normale” plutôt qu’exceptionnelle.
AbortController — annuler
Section intitulée « AbortController — annuler »Quand l’utilisateur quitte une page, ferme un modal, ou tape une nouvelle recherche, annule la requête en cours.
const controller = new AbortController();
fetch('/api/slow', { signal: controller.signal }) .then(r => r.json()) .catch(err => { if (err.name === 'AbortError') { console.log('Annulé'); } });
// Plus tardcontroller.abort();Cas d’usage : recherche au fil de la frappe
Section intitulée « Cas d’usage : recherche au fil de la frappe »let currentController = null;
input.addEventListener('input', async (e) => { // annule la requête précédente currentController?.abort(); currentController = new AbortController();
try { const r = await fetch(`/search?q=${e.target.value}`, { signal: currentController.signal, }); showResults(await r.json()); } catch (err) { if (err.name !== 'AbortError') console.error(err); }});Tu évites les race conditions où une vieille requête arrive après une nouvelle.
Timeout via AbortSignal
Section intitulée « Timeout via AbortSignal »// API moderne (depuis Node 17 / browsers récents)const r = await fetch('/api', { signal: AbortSignal.timeout(5000) });
// Avantconst c = new AbortController();setTimeout(() => c.abort(), 5000);fetch('/api', { signal: c.signal });Generators et async iterators (avancé, optionnel)
Section intitulée « Generators et async iterators (avancé, optionnel) »async function* lirePagination(url) { let next = url; while (next) { const r = await fetch(next); const { items, nextPage } = await r.json(); yield* items; next = nextPage; }}
for await (const item of lirePagination('/api/items')) { console.log(item);}Permet d’itérer paresseusement sur des données paginées, des streams, etc.
Auto-évaluation
Section intitulée « Auto-évaluation »Pour aller plus loin
Section intitulée « Pour aller plus loin »- MDN — Async JavaScript : developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous
- Promise visualizer — bevacqua.github.io/promisees/
- JavaScript Visualized: Event Loop — Lydia Hallie (vidéo)
- Loupe — Event Loop visualizer — latentflip.com/loupe
Suite : 6.3 — APIs du navigateur — DOM, fetch, storage, observers.