Esc
 Naviguer  Ouvrir Esc Fermer
Aller au contenu

6.2 — Asynchronisme

Débutant 35 min prérequis : axe 6.1 lu

🎯 Objectif : maîtriser Promises et async/await au 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

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.

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
Event loop : tâches synchrones, microtasks, macrotasks

Règle d’exécution :

  1. Le code synchrone tourne jusqu’à la fin de la pile d’appel.
  2. Toutes les microtasks sont exécutées (Promises résolues, queueMicrotask).
  3. Une macrotask est traitée (setTimeout callback, événement DOM, retour de fetch…).
  4. Retour à l’étape 1.
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');
// 1, 4, 3, 2
  • 1 et 4 : synchrone immédiat.
  • 3 : microtask, vidée avant la prochaine macrotask.
  • 2 : macrotask, au tour suivant.
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.

Une Promise = un objet qui représente une opération asynchrone et son futur résultat. 3 états :

  • pending : en cours
  • fulfilled : succès, avec une valeur
  • rejected : échec, avec une raison
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.

fetch('/api/users')
.then(response => response.json())
.then(users => console.log(users))
.catch(err => console.error(err))
.finally(() => console.log('Terminé'));
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.

// 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.

// ❌ Sériel : 3 secondes au total si chaque requête en prend 1
const a = await fetch('/a');
const b = await fetch('/b');
const c = await fetch('/c');
// ✅ Parallèle : 1 seconde au total
const [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);
});
// 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'),
]);
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évu
ids.forEach(async (id) => {
await fetchUser(id); // démarre tout, on ne sait pas quand ça finit
});
// ✅ Séquentiel
for (const id of ids) {
await fetchUser(id);
}
// ✅ Parallèle
await 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équentiel
for (const item of items) {
await db.execute({ sql: 'INSERT ...', args: [item] });
}
// ✅ Parallèle
await 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.

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
}
}
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);
});
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.

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 tard
controller.abort();
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.

// API moderne (depuis Node 17 / browsers récents)
const r = await fetch('/api', { signal: AbortSignal.timeout(5000) });
// Avant
const 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.

Tu lances 5 fetch en série avec await dans un for. Chacun prend 200 ms. Combien de temps au total ?
Quel ordre s'affiche ? `console.log('A'); setTimeout(() => console.log('B'), 0); Promise.resolve().then(() => console.log('C'));`
Tu lances 3 fetch avec Promise.all. La 2e échoue. Que se passe-t-il ?

Suite : 6.3 — APIs du navigateur — DOM, fetch, storage, observers.