Async/Await : la révolution de l'asynchrone en JavaScript
Avant async/await, gérer l'asynchrone en JavaScript ressemblait à ça :
fetchUser(userId, function(error, user) {
if (error) return handleError(error);
fetchPosts(user.id, function(error, posts) {
if (error) return handleError(error);
fetchComments(posts[0].id, function(error, comments) {
if (error) return handleError(error);
// 5 niveaux d'indentation plus tard...
});
});
});
Ce "callback hell" ou "pyramid of doom" rendait le code illisible et difficile à maintenir. Les Promises ont amélioré la situation, puis async/await en ES2017 a tout changé.
Ce guide couvre tout, des bases aux patterns avancés.
Les Promises : la fondation
Avant async/await, il faut comprendre les Promises. Une Promise représente une valeur qui sera disponible dans le futur (ou une erreur).
// Créer une Promise
const maPromesse = new Promise((resolve, reject) => {
setTimeout(() => {
const succes = Math.random() > 0.3;
if (succes) {
resolve({ id: 1, nom: 'Benjamin' });
} else {
reject(new Error('Impossible de charger l'utilisateur'));
}
}, 1000);
});
// Consommer une Promise
maPromesse
.then(user => console.log('Utilisateur :', user))
.catch(err => console.error('Erreur :', err.message))
.finally(() => console.log('Requête terminée'));
Les 3 états d'une Promise :
- Pending : en attente (valeur pas encore disponible)
- Fulfilled : résolue (
.then()est appelé) - Rejected : rejetée (
.catch()est appelé)
Async/Await : la syntaxe moderne
async/await est du "sucre syntaxique" au-dessus des Promises. Une fonction async retourne toujours une Promise. await suspend l'exécution jusqu'à ce que la Promise se résolve.
// Avec les Promises
function chargerUtilisateur(id) {
return fetch(`/api/users/${id}`)
.then(response => {
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
})
.then(data => data)
.catch(err => { throw err; });
}
// Avec async/await — identique mais lisible
async function chargerUtilisateur(id) {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
return data;
}
La clé : await ne peut être utilisé que dans une fonction async.
Gestion des erreurs avec try/catch
async function sauvegarderProfil(userId, donnees) {
try {
const response = await fetch(`/api/users/${userId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(donnees)
});
if (!response.ok) {
const erreur = await response.json();
throw new Error(erreur.message || `Erreur ${response.status}`);
}
const profil = await response.json();
console.log('Profil sauvegardé :', profil);
return profil;
} catch (err) {
if (err.name === 'TypeError') {
// Erreur réseau (pas de connexion)
console.error('Pas de connexion internet');
} else {
console.error('Erreur API :', err.message);
}
throw err; // Re-lancer pour que l'appelant puisse gérer
} finally {
masquerChargement(); // S'exécute toujours
}
}
Erreur fréquente : oublier le try/catch et laisser les rejections non gérées.
// ❌ Dangereux : erreur silencieuse
async function charger() {
const data = await fetch('/api/data').then(r => r.json());
return data;
}
// ✅ Gérer les erreurs explicitement
async function charger() {
try {
const response = await fetch('/api/data');
if (!response.ok) throw new Error(`Erreur ${response.status}`);
return await response.json();
} catch (err) {
console.error('Échec du chargement :', err);
return null; // ou throw selon le contexte
}
}
Parallélisme : Promise.all, Promise.allSettled, Promise.race
Promise.all — tout ou rien
Exécute plusieurs Promises en parallèle. Si UNE échoue, tout échoue.
// ❌ Séquentiel — lent (chaque await attend le précédent)
async function chargerDashboard(userId) {
const user = await chargerUtilisateur(userId); // 200ms
const posts = await chargerPosts(userId); // 300ms
const stats = await chargerStatistiques(userId); // 250ms
// Total : ~750ms
}
// ✅ Parallèle avec Promise.all — beaucoup plus rapide
async function chargerDashboard(userId) {
const [user, posts, stats] = await Promise.all([
chargerUtilisateur(userId), // chargerPosts(userId), // > lancés simultanément
chargerStatistiques(userId), // /
]);
// Total : ~300ms (le plus lent)
return { user, posts, stats };
}
Promise.allSettled — tolérant aux erreurs
Attend que toutes les Promises se terminent, qu'elles réussissent ou échouent :
async function chargerAvecFallback(urls) {
const resultats = await Promise.allSettled(
urls.map(url => fetch(url).then(r => r.json()))
);
return resultats.map((resultat, index) => {
if (resultat.status === 'fulfilled') {
return resultat.value;
} else {
console.warn(`URL ${urls[index]} a échoué :`, resultat.reason);
return null; // Valeur par défaut
}
});
}
Promise.race — le plus rapide gagne
// Timeout avec Promise.race
function avecTimeout(promise, ms) {
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error(`Timeout après ${ms}ms`)), ms)
);
return Promise.race([promise, timeout]);
}
// Utilisation
try {
const data = await avecTimeout(fetch('/api/lent'), 3000);
} catch (err) {
console.error('Requête trop lente :', err.message);
}
Promise.any — le premier succès
// Essayer plusieurs sources, prendre la première disponible
async function chargerDepuisMirrors(urls) {
try {
return await Promise.any(
urls.map(url => fetch(url).then(r => r.json()))
);
} catch (err) {
// AggregateError : toutes ont échoué
throw new Error('Tous les miroirs sont indisponibles');
}
}
Patterns avancés
Retry automatique
async function avecRetry(fn, options = {}) {
const { maxTentatives = 3, delai = 1000, facteur = 2 } = options;
let derniereErreur;
for (let tentative = 1; tentative <= maxTentatives; tentative++) {
try {
return await fn();
} catch (err) {
derniereErreur = err;
if (tentative < maxTentatives) {
const attente = delai * Math.pow(facteur, tentative - 1);
console.log(`Tentative ${tentative} échouée. Nouvel essai dans ${attente}ms...`);
await new Promise(r => setTimeout(r, attente));
}
}
}
throw new Error(`Échec après ${maxTentatives} tentatives : ${derniereErreur.message}`);
}
// Utilisation
const data = await avecRetry(
() => fetch('/api/instable').then(r => r.json()),
{ maxTentatives: 3, delai: 500 }
);
Cache simple pour les requêtes
const cache = new Map();
async function fetchAvecCache(url, ttl = 60000) {
const maintenant = Date.now();
const cached = cache.get(url);
if (cached && (maintenant - cached.timestamp) < ttl) {
return cached.data;
}
const response = await fetch(url);
const data = await response.json();
cache.set(url, { data, timestamp: maintenant });
return data;
}
AbortController — annuler une requête
// Indispensable pour les recherches en temps réel
let controlleur;
async function rechercherProduits(terme) {
// Annuler la requête précédente
if (controlleur) controlleur.abort();
controlleur = new AbortController();
try {
const response = await fetch(`/api/produits?q=${terme}`, {
signal: controlleur.signal
});
return await response.json();
} catch (err) {
if (err.name === 'AbortError') {
console.log('Requête annulée');
return null;
}
throw err;
}
}
// Dans un input de recherche
input.addEventListener('input', async (e) => {
const resultats = await rechercherProduits(e.target.value);
if (resultats) afficherResultats(resultats);
});
Async dans les boucles : les pièges
const ids = [1, 2, 3, 4, 5];
// ❌ forEach ne gère pas les Promises — ne PAS utiliser
ids.forEach(async (id) => {
const user = await chargerUtilisateur(id); // Non attendu !
console.log(user);
});
console.log('Terminé'); // S'affiche AVANT les utilisateurs
// ✅ for...of — séquentiel
for (const id of ids) {
const user = await chargerUtilisateur(id);
console.log(user);
}
// ✅ Promise.all + map — parallèle
const users = await Promise.all(ids.map(id => chargerUtilisateur(id)));
// ✅ Parallèle avec limite de concurrence
async function avecLimite(items, fn, limite = 3) {
const resultats = [];
for (let i = 0; i < items.length; i += limite) {
const lot = items.slice(i, i + limite);
const resultatsLot = await Promise.all(lot.map(fn));
resultats.push(...resultatsLot);
}
return resultats;
}
// Charger max 3 utilisateurs en parallèle
const users = await avecLimite(ids, chargerUtilisateur, 3);
Top-level await (modules ES2022)
// Dans un module ES (.mjs ou type="module")
// Plus besoin d'envelopper dans une fonction async !
const config = await fetch('/config.json').then(r => r.json());
const db = await connexionDB(config.dbUrl);
export { config, db };
Maîtriser async/await, c'est comprendre que JavaScript n'attend pas — il délègue. Chaque await dit au moteur : "je reviens quand cette Promise est résolue, vas-y fais autre chose en attendant." C'est ce qui permet à un seul thread de serveur de gérer des milliers de requêtes simultanées sans bloquer.
Pour aller plus loin : MDN async/await