Table of Contents
Introduction : L’evolution de l’asynchrone en JavaScript
L’histoire de la programmation asynchrone en JavaScript est une evolution fascinante qui reflete la maturation du langage. Comprendre cette evolution est essentiel pour apprecier pleinement la puissance d’async/await.
L’ere des Callbacks (avant ES6)
Au debut, JavaScript ne disposait que des callbacks pour gerer l’asynchrone. Cette approche, bien que fonctionnelle, menait souvent au fameux “callback hell” :
// Le callback hell - difficile a lire et maintenir
getUser(userId, function(err, user) {
if (err) {
handleError(err);
return;
}
getOrders(user.id, function(err, orders) {
if (err) {
handleError(err);
return;
}
getOrderDetails(orders[0].id, function(err, details) {
if (err) {
handleError(err);
return;
}
console.log(details);
});
});
});
L’arrivee des Promises (ES6)
ES6 a introduit les Promises, offrant une syntaxe plus elegante avec .then() et .catch() :
// Avec les Promises - meilleure lisibilite
getUser(userId)
.then(user => getOrders(user.id))
.then(orders => getOrderDetails(orders[0].id))
.then(details => console.log(details))
.catch(err => handleError(err));
async/await : La syntaxe moderne (ES2017)
Finalement, async/await a revolutionne la facon d’ecrire du code asynchrone en le rendant presque identique au code synchrone :
// Avec async/await - code clair et intuitif
async function getOrderInfo(userId) {
try {
const user = await getUser(userId);
const orders = await getOrders(user.id);
const details = await getOrderDetails(orders[0].id);
console.log(details);
} catch (err) {
handleError(err);
}
}
Dans cet article complet, nous allons explorer en profondeur async/await, des bases jusqu’aux patterns avances, en passant par les pieges courants a eviter.
Syntaxe de base d’async/await
Declaration d’une fonction async
Le mot-cle async transforme n’importe quelle fonction en une fonction qui retourne automatiquement une Promise. Voici les differentes syntaxes possibles :
// Declaration de fonction classique
async function fetchData() {
return 'donnees';
}
// Expression de fonction
const fetchData = async function() {
return 'donnees';
};
// Arrow function
const fetchData = async () => {
return 'donnees';
};
// Methode de classe
class ApiService {
async getData() {
return 'donnees';
}
}
// Methode d'objet
const api = {
async fetch() {
return 'donnees';
}
};
Utilisation de await
Le mot-cle await ne peut etre utilise qu’a l’interieur d’une fonction async. Il met en pause l’execution de la fonction jusqu’a ce que la Promise soit resolue :
async function newUnicorn() {
// await met en pause jusqu'a ce que fetch soit complete
const response = await fetch('https://api.example.com/unicorns');
// Puis attend que response.json() soit complete
const json = await response.json();
// Retourne la valeur (encapsulee dans une Promise)
return json.success;
}
// Appel de la fonction async
newUnicorn()
.then(success => console.log('Succes:', success))
.catch(err => console.error('Erreur:', err));
// Ou depuis une autre fonction async
async function main() {
const success = await newUnicorn();
console.log('Succes:', success);
}
await au niveau superieur (Top-level await)
Depuis ES2022, vous pouvez utiliser await au niveau superieur dans les modules ES :
// module.mjs
const response = await fetch('https://api.example.com/config');
export const config = await response.json();
// Attention : cela bloque l'import du module
Gestion des erreurs avec try/catch
La gestion des erreurs est cruciale dans le code asynchrone. Avec async/await, vous utilisez les blocs try/catch familiars :
Pattern de base
async function fetchUserData(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
// Verifier si la reponse est OK (status 200-299)
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
// Gerer les erreurs reseau et les erreurs HTTP
console.error('Erreur lors de la recuperation:', error.message);
throw error; // Re-lancer pour permettre au code appelant de gerer
}
}
Gestion granulaire des erreurs
async function complexOperation() {
let connection;
try {
connection = await database.connect();
try {
const result = await connection.query('SELECT * FROM users');
return result;
} catch (queryError) {
console.error('Erreur de requete:', queryError);
throw new Error('La requete a echoue');
}
} catch (connectionError) {
console.error('Erreur de connexion:', connectionError);
throw new Error('Impossible de se connecter a la base');
} finally {
// Toujours nettoyer, meme en cas d'erreur
if (connection) {
await connection.close();
}
}
}
Pattern avec valeur par defaut
async function fetchWithDefault(url, defaultValue) {
try {
const response = await fetch(url);
return await response.json();
} catch (error) {
console.warn(`Utilisation de la valeur par defaut:`, error.message);
return defaultValue;
}
}
// Utilisation
const users = await fetchWithDefault('/api/users', []);
const config = await fetchWithDefault('/api/config', { theme: 'light' });
Patterns courants d’execution asynchrone
Execution sequentielle vs parallele
Comprendre la difference entre l’execution sequentielle et parallele est fondamental pour optimiser vos performances.
Execution sequentielle (une a la fois)
// SEQUENTIEL - Les requetes s'executent l'une apres l'autre
async function fetchSequential(urls) {
const results = [];
for (const url of urls) {
const response = await fetch(url); // Attend chaque requete
const data = await response.json();
results.push(data);
}
return results;
}
// Temps total = temps1 + temps2 + temps3 + ...
Execution parallele (toutes en meme temps)
// PARALLELE - Toutes les requetes demarrent en meme temps
async function fetchParallel(urls) {
const promises = urls.map(url => fetch(url).then(r => r.json()));
const results = await Promise.all(promises);
return results;
}
// Temps total = max(temps1, temps2, temps3, ...)
Le piege de forEach avec await
L’utilisation de forEach avec await est un piege classique :
// NE FAITES PAS CELA - forEach n'attend pas les promises
async function badExample() {
const data = [1, 2, 3, 4, 5];
data.forEach(async (e) => {
const result = await somePromiseFn(e);
console.log(result);
});
console.log('Termine'); // S'affiche AVANT les resultats!
}
// FAITES CELA A LA PLACE - Utilisez for...of pour sequentiel
async function goodSequential() {
const data = [1, 2, 3, 4, 5];
for (const e of data) {
const result = await somePromiseFn(e);
console.log(result);
}
console.log('Termine'); // S'affiche APRES tous les resultats
}
// Ou map + Promise.all pour parallele
async function goodParallel() {
const data = [1, 2, 3, 4, 5];
const results = await Promise.all(
data.map(async (e) => {
const result = await somePromiseFn(e);
return result;
})
);
console.log('Resultats:', results);
console.log('Termine');
}
Promise.all - Execution parallele avec echec rapide
Promise.all execute plusieurs Promises en parallele et echoue des que l’une d’elles echoue :
async function getFriendPosts(user) {
const friendIds = await db.get("friends", {user}, {id: 1});
// Toutes les requetes partent en meme temps
const posts = await Promise.all(
friendIds.map(id => db.get("posts", {user: id}))
);
return posts.flat();
}
// Exemple avec gestion d'erreur
async function fetchMultipleUrls(urls) {
try {
const results = await Promise.all(
urls.map(async url => {
const response = await fetch(url);
if (!response.ok) throw new Error(`Failed: ${url}`);
return response.json();
})
);
return results;
} catch (error) {
// Une seule erreur fait echouer tout le Promise.all
console.error('Au moins une requete a echoue:', error);
throw error;
}
}
Promise.race - Premier arrive, premier servi
Promise.race retourne le resultat de la premiere Promise qui se resout (ou rejette) :
// Utile pour implementer un timeout
async function fetchWithTimeout(url, timeoutMs = 5000) {
const fetchPromise = fetch(url);
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error('Timeout!')), timeoutMs);
});
return Promise.race([fetchPromise, timeoutPromise]);
}
// Utilisation
try {
const response = await fetchWithTimeout('/api/slow-endpoint', 3000);
const data = await response.json();
} catch (error) {
if (error.message === 'Timeout!') {
console.error('La requete a pris trop de temps');
}
}
Promise.allSettled - Recuperer tous les resultats
Promise.allSettled attend que toutes les Promises soient terminees, peu importe leur statut :
async function fetchAllWithStatus(urls) {
const results = await Promise.allSettled(
urls.map(url => fetch(url).then(r => r.json()))
);
// Chaque resultat a un status: 'fulfilled' ou 'rejected'
const successful = results
.filter(r => r.status === 'fulfilled')
.map(r => r.value);
const failed = results
.filter(r => r.status === 'rejected')
.map(r => r.reason);
console.log(`Succes: ${successful.length}, Echecs: ${failed.length}`);
return { successful, failed };
}
// Exemple pratique: envoi de notifications
async function sendNotifications(users) {
const results = await Promise.allSettled(
users.map(user => sendNotification(user))
);
const report = {
sent: results.filter(r => r.status === 'fulfilled').length,
failed: results.filter(r => r.status === 'rejected').length
};
return report;
}
Promise.any - Premier succes
Promise.any retourne la premiere Promise qui reussit (ignore les rejets) :
// Utile pour les fallbacks ou la redondance
async function fetchFromMultipleSources(sources) {
try {
// Retourne le premier serveur qui repond avec succes
const data = await Promise.any(
sources.map(source => fetch(source).then(r => r.json()))
);
return data;
} catch (error) {
// AggregateError si TOUTES les promises ont echoue
console.error('Toutes les sources ont echoue:', error.errors);
throw error;
}
}
// Exemple: serveur le plus rapide
async function fetchFromFastestServer() {
const servers = [
'https://server1.example.com/api/data',
'https://server2.example.com/api/data',
'https://server3.example.com/api/data'
];
return Promise.any(servers.map(url => fetch(url).then(r => r.json())));
}
Iterateurs asynchrones et generateurs
Les iterateurs asynchrones permettent de traiter des flux de donnees asynchrones de maniere elegante et efficace.
for await…of - Iteration asynchrone
La boucle for await...of permet d’iterer sur des sources de donnees asynchrones :
// Exemple avec un generateur asynchrone
async function* delayedRange(max) {
for (let i = 0; i < max; i++) {
await delay(1000);
yield i;
}
}
// Utilisation avec for await...of
async function processRange() {
for await (const num of delayedRange(5)) {
console.log(`Nombre recu: ${num}`);
// S'affiche toutes les secondes: 0, 1, 2, 3, 4
}
}
Generateurs asynchrones
Les generateurs asynchrones combinent async et function* pour creer des iterateurs puissants :
// Generateur pour paginer une API
async function* fetchPaginatedData(baseUrl) {
let page = 1;
let hasMore = true;
while (hasMore) {
const response = await fetch(`${baseUrl}?page=${page}`);
const data = await response.json();
yield* data.items; // Yield chaque item individuellement
hasMore = data.hasNextPage;
page++;
}
}
// Utilisation - traite les donnees au fur et a mesure
async function processAllData() {
for await (const item of fetchPaginatedData('/api/items')) {
await processItem(item);
console.log(`Traite: ${item.id}`);
}
}
Lecture de fichiers en streaming
// Lire un fichier ligne par ligne (Node.js)
async function* readLines(filePath) {
const fileStream = fs.createReadStream(filePath);
const rl = readline.createInterface({ input: fileStream });
for await (const line of rl) {
yield line;
}
}
// Utilisation
async function processLargeFile() {
let lineCount = 0;
for await (const line of readLines('./large-file.txt')) {
lineCount++;
if (line.includes('ERROR')) {
console.log(`Erreur ligne ${lineCount}: ${line}`);
}
}
}
Transformer des streams asynchrones
// Generateur de transformation
async function* filterAndTransform(source, predicate, transformer) {
for await (const item of source) {
if (predicate(item)) {
yield transformer(item);
}
}
}
// Pipeline de traitement
async function processDataPipeline() {
const source = fetchPaginatedData('/api/users');
const activeUsers = filterAndTransform(
source,
user => user.isActive,
user => ({ id: user.id, name: user.name.toUpperCase() })
);
for await (const user of activeUsers) {
console.log(user);
}
}
Patterns avances
Retry avec backoff exponentiel
Implementez une logique de retry robuste pour les operations qui peuvent echouer temporairement :
async function retryWithBackoff(
fn,
maxRetries = 3,
baseDelay = 1000,
maxDelay = 30000
) {
let lastError;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error;
// Ne pas retry si c'est une erreur permanente
if (error.status === 400 || error.status === 401) {
throw error;
}
// Calcul du delai avec jitter aleatoire
const delay = Math.min(
baseDelay * Math.pow(2, attempt) + Math.random() * 1000,
maxDelay
);
console.log(`Tentative ${attempt + 1} echouee. Retry dans ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw lastError;
}
// Utilisation
const data = await retryWithBackoff(
async () => {
const response = await fetch('/api/unstable-endpoint');
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
},
5, // maxRetries
1000, // baseDelay (1s)
30000 // maxDelay (30s)
);
Timeout avec Promise.race
Pattern reutilisable pour ajouter un timeout a n’importe quelle Promise :
function withTimeout(promise, ms, errorMessage = 'Operation timed out') {
const timeout = new Promise((_, reject) => {
const id = setTimeout(() => {
clearTimeout(id);
reject(new Error(errorMessage));
}, ms);
});
return Promise.race([promise, timeout]);
}
// Utilisation
async function fetchWithTimeout(url, options = {}, timeoutMs = 5000) {
const controller = new AbortController();
const { signal } = controller;
const timeout = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(url, { ...options, signal });
return response;
} finally {
clearTimeout(timeout);
}
}
// Exemple avec AbortController pour annuler proprement
async function fetchData() {
try {
const response = await fetchWithTimeout('/api/data', {}, 3000);
return await response.json();
} catch (error) {
if (error.name === 'AbortError') {
console.error('Requete annulee (timeout)');
}
throw error;
}
}
Queue de taches asynchrones
Gerez l’execution controlee de taches avec une limite de concurrence :
class AsyncQueue {
constructor(concurrency = 3) {
this.concurrency = concurrency;
this.running = 0;
this.queue = [];
}
async add(task) {
return new Promise((resolve, reject) => {
this.queue.push({ task, resolve, reject });
this.process();
});
}
async process() {
if (this.running >= this.concurrency || this.queue.length === 0) {
return;
}
const { task, resolve, reject } = this.queue.shift();
this.running++;
try {
const result = await task();
resolve(result);
} catch (error) {
reject(error);
} finally {
this.running--;
this.process();
}
}
}
// Utilisation
const queue = new AsyncQueue(3); // Max 3 taches en parallele
const urls = ['url1', 'url2', 'url3', 'url4', 'url5'];
const results = await Promise.all(
urls.map(url => queue.add(() => fetch(url).then(r => r.json())))
);
Semaphore pour limiter la concurrence
class Semaphore {
constructor(max) {
this.max = max;
this.count = 0;
this.waiting = [];
}
async acquire() {
if (this.count < this.max) {
this.count++;
return;
}
await new Promise(resolve => this.waiting.push(resolve));
this.count++;
}
release() {
this.count--;
if (this.waiting.length > 0) {
const next = this.waiting.shift();
next();
}
}
async use(fn) {
await this.acquire();
try {
return await fn();
} finally {
this.release();
}
}
}
// Utilisation - limiter les requetes API
const apiLimiter = new Semaphore(5);
async function fetchWithLimit(url) {
return apiLimiter.use(async () => {
const response = await fetch(url);
return response.json();
});
}
Bonnes pratiques
1. Toujours gerer les erreurs
Ne laissez jamais une Promise sans gestion d’erreur :
// MAL - Erreur silencieuse
async function bad() {
fetchData(); // Promise non attendue, erreur perdue
}
// BIEN - Erreur geree
async function good() {
try {
await fetchData();
} catch (error) {
logger.error('Fetch failed:', error);
// Gerer l'erreur appropriement
}
}
2. Eviter les awaits sequentiels inutiles
// LENT - Execution sequentielle
const user = await getUser(id);
const posts = await getPosts(id);
const followers = await getFollowers(id);
// RAPIDE - Execution parallele
const [user, posts, followers] = await Promise.all([
getUser(id),
getPosts(id),
getFollowers(id)
]);
3. Utiliser Promise.allSettled pour les operations non-critiques
// Si une erreur ne doit pas bloquer les autres
const results = await Promise.allSettled([
sendEmail(user),
updateAnalytics(event),
syncWithCRM(data)
]);
// Verifier les erreurs apres
results.forEach((result, index) => {
if (result.status === 'rejected') {
logger.warn(`Operation ${index} failed:`, result.reason);
}
});
4. Preferer les fonctions nommees pour le debugging
// Les stack traces sont plus claires avec des fonctions nommees
const fetchUsers = async function fetchUsers() {
return await api.get('/users');
};
// Plutot que des fonctions anonymes
const fetchUsers = async () => await api.get('/users');
5. Eviter async dans les constructeurs
// MAUVAIS - Les constructeurs ne peuvent pas etre async
class BadExample {
constructor() {
this.data = await fetchData(); // SyntaxError!
}
}
// BIEN - Utiliser une methode factory
class GoodExample {
constructor(data) {
this.data = data;
}
static async create() {
const data = await fetchData();
return new GoodExample(data);
}
}
const instance = await GoodExample.create();
6. Nettoyer les ressources avec finally
async function processWithCleanup() {
const connection = await database.connect();
try {
await connection.query('...');
await connection.query('...');
} finally {
// Toujours nettoyer, meme en cas d'erreur
await connection.close();
}
}
Pieges courants a eviter
1. Oublier await
// PIEGE - La fonction retourne avant que le fetch soit termine
async function fetchData() {
const response = fetch('/api/data'); // Oubli de await!
return response.json(); // Erreur: response est une Promise, pas une Response
}
// CORRECT
async function fetchData() {
const response = await fetch('/api/data');
return response.json();
}
2. try/catch autour du mauvais code
// PIEGE - L'erreur n'est pas attrapee
async function bad() {
try {
const promise = fetchData();
} catch (error) {
console.log('Caught!'); // Ne sera jamais execute
}
await promise; // L'erreur est lancee ici, en dehors du try/catch
}
// CORRECT
async function good() {
try {
const result = await fetchData();
} catch (error) {
console.log('Caught!'); // Sera execute si fetchData echoue
}
}
3. Creer des deadlocks accidentels
// PIEGE - Deadlock potentiel
async function deadlock() {
const lockA = await acquireLock('A');
const lockB = await acquireLock('B'); // Si un autre thread a B et attend A...
// Code...
await releaseLock('B');
await releaseLock('A');
}
// MIEUX - Utiliser un timeout ou un ordre d'acquisition coherent
async function safer() {
const locks = await Promise.race([
acquireBothLocks('A', 'B'),
timeout(5000, 'Lock acquisition timeout')
]);
try {
// Code...
} finally {
await releaseAllLocks(locks);
}
}
4. Lancer des erreurs non-Error
// PIEGE - Difficile a debugger
async function bad() {
throw 'Something went wrong'; // String au lieu de Error
throw { message: 'error' }; // Object sans stack trace
}
// CORRECT - Toujours lancer des objets Error
async function good() {
throw new Error('Something went wrong');
throw new CustomError('Specific error', { code: 'ERR_001' });
}
5. Ignorer les Promises non attendues
// PIEGE - L'erreur est silencieuse
async function handler(req, res) {
saveToDatabase(req.body); // Pas d'await, pas de catch
res.send('OK');
}
// CORRECT - Gerer toutes les Promises
async function handler(req, res) {
try {
await saveToDatabase(req.body);
res.send('OK');
} catch (error) {
res.status(500).send('Error saving data');
}
}
Conclusion
async/await a revolutionne la programmation asynchrone en JavaScript, rendant le code plus lisible et maintenable. Voici un tableau comparatif des trois approches :
| Critere | Callbacks | Promises | async/await |
|---|---|---|---|
| Lisibilite | Faible (callback hell) | Moyenne (chaines) | Excellente (synchrone-like) |
| Gestion erreurs | Manuelle (error-first) | .catch() | try/catch natif |
| Debugging | Difficile | Moyen | Facile (stack traces) |
| Composition | Complexe | Bonne | Excellente |
| Annulation | Non standard | Non standard | AbortController |
| Iteration | Tres complexe | Possible | for await...of |
Points cles a retenir
- async/await est du sucre syntaxique sur les Promises - les deux sont interoperables
- Privilegiez l’execution parallele avec
Promise.allquand les operations sont independantes - Gerez toujours les erreurs avec try/catch ou
.catch() - Evitez forEach avec await - utilisez
for...ofoumap+Promise.all - Utilisez les generateurs asynchrones pour les streams de donnees
- Implementez des patterns de retry pour les operations instables
- Limitez la concurrence avec des semaphores ou des queues
En maitrisant ces concepts et patterns, vous serez capable d’ecrire du code asynchrone robuste, performant et maintenable qui repond aux exigences des applications modernes.
In-Article Ad
Dev Mode
Tags
Mahmoud DEVO
Senior Full-Stack Developer
I'm a passionate full-stack developer with 10+ years of experience building scalable web applications. I write about Vue.js, Node.js, PostgreSQL, and modern DevOps practices.
Enjoyed this article?
Subscribe to get more tech content delivered to your inbox.
Related Articles
JavaScript Promises : Guide Complet de la Programmation Asynchrone
Maitrisez les Promises JavaScript : creation, chainage, gestion d'erreurs, Promise.all et patterns avances pour un code asynchrone elegant.
Manipulation des dates en JavaScript : UTC, conversion et formatage
Guide complet sur les dates JavaScript : conversion en chaine, creation de dates UTC, methodes setUTC et bonnes pratiques pour eviter les problemes de fuseaux.
La comparaison équivalente en JavaScript : une analyse appro
Here's a compelling meta description that summarizes the main value proposition, includes a subtle call-to-action, and meets the 150-160 character requirement: