Maîtrisez les Promises et Async/Await pour gérer le code asynchrone JavaScript : chaînes de promesses, gestion des erreurs, Promise.all et éviter le callback hell.
Les Promises : qu'est-ce que c'est ?
Une Promise en JavaScript représente la finalisation (ou l'échec) d'une opération asynchrone et sa valeur résultante. C'est un objet qui relie un callback asynchrone à du code réactif.
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Succès!');
}, 1000);
});
promise.then(result => {
console.info(result); // 'Succès!'
});
États d'une Promise : pending, fulfilled, rejected
Une Promise possède 3 états possibles :
- pending : opération en cours
- fulfilled : opération réussie (resolve)
- rejected : opération échouée (reject)
// ✓ Fulfilled
new Promise(resolve => {
resolve('Valeur');
}).then(value => console.info(value));
// ✗ Rejected
new Promise((resolve, reject) => {
reject(new Error('Erreur!'));
}).catch(error => console.error(error));
// Pending (ne terminera jamais)
new Promise(() => {
// Rien ne se passe
});
Methods des Promises : then, catch, finally
fetch('https://api.example.com/data')
.then(response => response.json()) // Chaîner then
.then(data => console.info(data))
.catch(error => console.error('Erreur:', error)) // Gérer l'erreur
.finally(() => console.info('Terminé')); // Toujours exécuté
Promise.all() : attendre plusieurs Promises
Promise.all([
fetch('/api/users').then(r => r.json()),
fetch('/api/posts').then(r => r.json()),
fetch('/api/comments').then(r => r.json())
])
.then(([users, posts, comments]) => {
console.info(users, posts, comments);
});
Async/Await : la syntaxe moderne
async/await est une syntaxe plus lisible pour travailler avec les Promises.
// Avec Promises
function fetchData() {
return fetch('/api/data')
.then(r => r.json())
.then(data => console.info(data));
}
// Avec async/await
async function fetchData() {
const response = await fetch('/api/data');
const data = await response.json();
console.info(data);
}
async marque une fonction comme asynchrone. await pause l'exécution jusqu'à ce que la Promise soit résolue.
Promises vs Async/Await : comparaison
| Aspect | Promises | Async/Await |
|---|---|---|
| Lisibilité | Chaînes de .then() | Code synchrone-like |
| Erreurs | .catch() | try/catch |
| Parallélisation | Promise.all() | Promise.all() |
| Debugging | Stacktraces moins claires | Stacktraces claires |
Gestion des erreurs
Avec Promises :
fetch('/api/data')
.then(r => r.json())
.catch(error => {
console.error('Erreur:', error.message);
});
Avec Async/Await :
async function getData() {
try {
const response = await fetch('/api/data');
const data = await response.json();
return data;
} catch (error) {
console.error('Erreur:', error.message);
return null;
}
}
Patterns et cas d'usage avancés
Pattern 1 : Retry avec backoff exponentiel
// Retry automatique avec délai croissant
async function fetchWithRetry(url, options = {}, maxAttempts = 3) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
const response = await fetch(url, options);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return await response.json();
} catch (error) {
if (attempt === maxAttempts) throw error;
// Délai exponentiel : 1s, 2s, 4s...
const delay = Math.pow(2, attempt - 1) * 1000;
console.warn(`Attempt ${attempt} failed, retrying in ${delay}ms`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
// Usage
const data = await fetchWithRetry('/api/data', {}, 3);
Pattern 2 : Timeout avec AbortController
// Annuler une requête si elle dépasse un délai
async function fetchWithTimeout(url, timeoutMs = 5000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(url, { signal: controller.signal });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return await response.json();
} catch (error) {
if (error.name === 'AbortError') {
throw new Error(`Request timeout after ${timeoutMs}ms`);
}
throw error;
} finally {
clearTimeout(timeoutId); // Toujours nettoyer le timer
}
}
Promise.allSettled() — résultats même en cas d'erreur
// allSettled() attend TOUTES les promises, même les rejetées
const results = await Promise.allSettled([
fetch('/api/users').then(r => r.json()),
fetch('/api/posts').then(r => r.json()),
fetch('/api/broken-endpoint').then(r => r.json()) // Échouera
]);
// Chaque résultat a un status 'fulfilled' ou 'rejected'
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.info(`API ${index}: OK`, result.value);
} else {
console.error(`API ${index}: ERREUR`, result.reason);
}
});
// Extraire uniquement les succès
const successes = results
.filter(r => r.status === 'fulfilled')
.map(r => r.value);
| Méthode | Comportement sur erreur | Cas d'usage |
|---|---|---|
Promise.all() | Rejette dès la première erreur | Données interdépendantes |
Promise.allSettled() | Attend toutes, retourne statuts | Données indépendantes avec fallback |
Promise.race() | Première qui résout/rejette gagne | Timeout, cache vs réseau |
Promise.any() | Première résolue gagne, ignore les rejets | Redondance, multi-CDN |
Promise.race() — course entre promesses
// Utiliser le cache local si plus rapide que le réseau
function fetchFromCacheOrNetwork(key, networkUrl) {
const cachePromise = caches.open('v1').then(cache => cache.match(key));
const networkPromise = fetch(networkUrl).then(r => r.json());
return Promise.race([cachePromise, networkPromise]);
}
// Pattern timeout avec race()
function withTimeout(promise, ms) {
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error(`Timeout ${ms}ms`)), ms)
);
return Promise.race([promise, timeout]);
}
// Usage
const data = await withTimeout(fetchData(), 3000);
Async generators — flux de données
// Async generator : yield des valeurs asynchrones
async function* fetchPages(baseUrl, maxPages = 5) {
for (let page = 1; page <= maxPages; page++) {
const response = await fetch(`${baseUrl}?page=${page}`);
const data = await response.json();
if (data.items.length === 0) return; // Arrêt si page vide
yield data.items;
}
}
// Consommation avec for await...of
async function loadAllArticles() {
const allArticles = [];
for await (const pageItems of fetchPages('/api/articles')) {
allArticles.push(...pageItems);
console.info(`Chargés: ${allArticles.length} articles`);
}
return allArticles;
}
// Streaming de résultats
async function streamSearch(query) {
for await (const batch of fetchPages(`/api/search?q=${query}`)) {
renderBatch(batch); // Afficher au fur et à mesure
}
}
TypeScript : typer l'asynchrone correctement
// Types pour les fonctions async
interface ApiResponse<T> {
data: T;
status: number;
message: string;
}
// Fonction async typée
async function fetchUser(id: number): Promise<ApiResponse<User>> {
const response = await fetch(`/api/users/${id}`);
return response.json();
}
// Gestion d'erreur typée
type Result<T, E = Error> =
| { success: true; data: T }
| { success: false; error: E };
async function safeApiFetch<T>(url: string): Promise<Result<T>> {
try {
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data: T = await response.json();
return { success: true, data };
} catch (error) {
return { success: false, error: error as Error };
}
}
// Usage type-safe
const result = await safeApiFetch<User[]>('/api/users');
if (result.success) {
result.data.forEach(user => console.info(user.name));
} else {
console.error(result.error.message);
}
Conclusion
Async/await est la syntaxe à préférer pour la lisibilité, mais les méthodes statiques de Promise (all, allSettled, race, any) restent indispensables pour les patterns parallèles. En TypeScript, le type Result<T> rend la gestion d'erreur explicite et type-safe sans exceptions.
await ou .catch(). Une Promise non gérée peut causer des bugs silencieux difficiles à débuguer.