Si vous écrivez du JavaScript depuis plus de quelques mois, vous avez ressenti la douleur des callbacks profondément imbriqués et des chaînes .then() emmêlées. async/await, introduit dans ES2017, a résolu tout cela — et pourtant les développeurs tombent encore dans les mêmes trois ou quatre pièges. Passons en revue l'ensemble du sujet correctement : comment ça fonctionne, comment gérer les erreurs correctement, et les schémas d'exécution parallèle qui comptent vraiment pour les performances.
Les bases — fonctions async et await
Une fonction async retourne toujours une Promise. À l'intérieur, await met en pause l'exécution jusqu'à ce que la Promise attendue soit résolue. C'est tout le modèle mental :
async function fetchUserProfile(userId) {
const response = await fetch(`https://api.example.com/users/${userId}`);
const profile = await response.json();
return profile; // wrapped in a Promise automatically
}
// Calling an async function gives you a Promise
const profilePromise = fetchUserProfile(42);
profilePromise.then(profile => console.log(profile.name));
// Or use await at the call site
const profile = await fetchUserProfile(42);
console.log(profile.name);Le mot-clé await ne peut être utilisé qu'à l'intérieur d'une fonction async — ou au niveau supérieur d'un module ES (nous y reviendrons plus tard). L'utiliser ailleurs est une erreur de syntaxe.
Gestion des erreurs — La bonne et la mauvaise façon
C'est là que la plupart des tutoriels se perdent. L'instinct est d'envelopper tout dans try/catch et d'en finir. Ça fonctionne, mais les blocs catch vides sont une mauvaise odeur de code qui masque de vrais bugs :
// ❌ Don't do this — silent failure, impossible to debug
async function loadConfig() {
try {
const res = await fetch('/api/config');
return await res.json();
} catch (err) {
// swallowed — you'll never know what broke
}
}
// ✅ Do this — handle errors explicitly, return something meaningful
async function loadConfig() {
try {
const res = await fetch('/api/config');
if (!res.ok) {
throw new Error(`Config fetch failed: ${res.status} ${res.statusText}`);
}
return await res.json();
} catch (err) {
console.error('loadConfig error:', err.message);
return null; // caller can check for null
}
}Je préfère un schéma où les fonctions async retournent soit null en cas d'échec, soit lèvent des erreurs intentionnelles. Ce que j'évite, c'est de capturer une erreur, de la journaliser, puis de retourner une valeur qui laisse penser à l'appelant que la requête a réussi.
await to(promise) qui retourne des tuples [error, data].Séquentiel vs Parallèle — Le piège de performance
C'est l'erreur que je vois le plus souvent dans le code de production. Quand vous await chaque appel l'un après l'autre, vous les exécutez séquentiellement — même quand ils sont complètement indépendants les uns des autres :
// ❌ Sequential — takes ~900ms total (300 + 300 + 300)
async function loadDashboard(userId) {
const user = await fetchUser(userId); // 300ms
const orders = await fetchOrders(userId); // 300ms
const settings = await fetchSettings(userId); // 300ms
return { user, orders, settings };
}
// ✅ Parallel with Promise.all — takes ~300ms total
async function loadDashboard(userId) {
const [user, orders, settings] = await Promise.all([
fetchUser(userId),
fetchOrders(userId),
fetchSettings(userId)
]);
return { user, orders, settings };
}Promise.all() lance les trois requêtes au même moment et attend que toutes soient terminées. Si l'une d'elles est rejetée, l'ensemble est rejeté. C'est généralement ce que vous voulez pour un chargement de type tableau de bord où toutes les données sont nécessaires.
Promise.allSettled — Quand l'échec partiel est acceptable
Parfois vous voulez lancer plusieurs requêtes et utiliser ce qui revient avec succès, même si certaines échouent. Promise.allSettled() est conçu exactement pour ça :
async function loadWidgets(widgetIds) {
const results = await Promise.allSettled(
widgetIds.map(id => fetchWidget(id))
);
const widgets = [];
const errors = [];
for (const result of results) {
if (result.status === 'fulfilled') {
widgets.push(result.value);
} else {
errors.push(result.reason.message);
}
}
if (errors.length > 0) {
console.warn('Some widgets failed to load:', errors);
}
return widgets; // return whatever succeeded
}Ce schéma est idéal pour les éléments d'interface non critiques — comme une barre latérale avec plusieurs sections indépendantes. Si l'une échoue, vous affichez le reste au lieu de vider toute la page.
async dans les boucles — Le piège de forEach
Ça a brûlé tout le monde au moins une fois. Array.forEach() n'attend pas les callbacks async — il les lance et passe immédiatement à la suite. La boucle se termine avant que tout le travail async soit fait :
const orderIds = [101, 102, 103, 104];
// ❌ forEach ignores async — all requests fire in parallel uncontrolled,
// and code after the forEach runs before any complete
orderIds.forEach(async (id) => {
await processOrder(id); // NOT awaited by forEach
});
console.log('done?'); // prints before any order is processed
// ✅ for...of — sequential, fully awaited
for (const id of orderIds) {
await processOrder(id);
}
console.log('done'); // prints after all orders are processed
// ✅ Parallel but controlled — all fire at once, await all completions
await Promise.all(orderIds.map(id => processOrder(id)));
console.log('done'); // prints after all orders are processedUtilisez for...of quand l'ordre importe ou quand vous avez besoin de limiter les requêtes (traiter une à la fois). Utilisez Promise.all(map(...)) quand vous voulez un maximum de parallélisme et n'avez pas besoin de garanties séquentielles.
await au niveau supérieur dans les modules ES
Depuis ES2022, vous pouvez utiliser await au niveau supérieur d'un module ES — pas besoin de fonction enveloppante. C'est important pour l'initialisation de modules qui dépend de données async :
// config.js (ES module)
const response = await fetch('/api/runtime-config');
const config = await response.json();
export const API_BASE_URL = config.apiBaseUrl;
export const FEATURE_FLAGS = config.featureFlags;// main.js — imports wait for config.js to fully resolve
import { API_BASE_URL, FEATURE_FLAGS } from './config.js';
console.log(API_BASE_URL); // guaranteed to be loadedL'await de niveau supérieur fonctionne dans Node.js 14.8+ avec "type": "module" dans package.json, et dans tous les navigateurs modernes via les modules ES natifs. L'exécution du module importateur est mise en pause jusqu'à ce que le module attendu soit entièrement résolu — c'est exactement la garantie dont vous avez besoin.
Un vrai pipeline — Récupération, analyse et transformation
Voici un pipeline async réaliste qui combine tout : récupération depuis une API, gestion des erreurs HTTP, transformation des données et repli gracieux en cas d'échec :
async function getProductCatalog(categoryId) {
// Step 1: fetch raw data
const response = await fetch(
`https://api.shop.example.com/categories/${categoryId}/products`,
{ headers: { Authorization: `Bearer ${getAuthToken()}` } }
);
if (!response.ok) {
throw new Error(`Catalog fetch failed: ${response.status}`);
}
// Step 2: parse JSON
const raw = await response.json();
// Step 3: transform into the shape your UI needs
const products = raw.items.map(item => ({
id: item.product_id,
name: item.display_name,
price: (item.price_cents / 100).toFixed(2),
inStock: item.inventory_count > 0,
imageUrl: item.media?.[0]?.url ?? '/images/placeholder.png'
}));
// Step 4: filter out anything that's been discontinued
return products.filter(p => !p.discontinued);
}
// Usage
try {
const catalog = await getProductCatalog('electronics');
renderProductGrid(catalog);
} catch (err) {
showErrorBanner(`Could not load products: ${err.message}`);
}Outils utiles
Lorsque vous déboguez du code async qui traite des charges JSON, ces outils aident : Formateur JSON pour inspecter les réponses API, Validateur JSON pour détecter les charges mal formées avant qu'elles atteignent votre code, et Formateur JS pour nettoyer le code de fonctions async. Pour la spécification complète d'async/await, le guide async MDN est la référence la plus complète, et la spécification TC39 couvre la sémantique exacte si vous en avez besoin.
Conclusion
async/await rend le JavaScript asynchrone lisible — mais les pièges sont réels. Vérifiez toujours response.ok avant d'analyser, ne laissez jamais les blocs catch avaler les erreurs silencieusement, utilisez Promise.all() pour les appels parallèles indépendants, et évitez forEach avec des callbacks async. Ancrez ces habitudes et votre code async sera à la fois rapide et déboguable.