[Medium] 📄 Async/Await
💡 Il est recommandé de lire d'abord Promise pour comprendre les concepts de base
Qu'est-ce que async/await ?
async/await est un sucre syntaxique introduit dans ES2017 (ES8), construit sur les Promise, qui fait ressembler le code asynchrone à du code synchrone, le rendant plus facile à lire et à maintenir.
Concepts clés :
- Les fonctions
asyncretournent toujours une Promise awaitne peut être utilisé qu'à l'intérieur des fonctionsasyncawaitmet en pause l'exécution de la fonction et attend que la Promise soit résolue
Syntaxe de base
Fonction async
Le mot-clé async fait qu'une fonction retourne automatiquement une Promise :
// Écriture traditionnelle avec Promise
function fetchData() {
return Promise.resolve('données');
}
// Écriture avec async (équivalent)
async function fetchData() {
return 'données'; // Automatiquement encapsulé dans Promise.resolve('données')
}
// La façon d'appeler est identique
fetchData().then((data) => console.log(data)); // 'données'
Mot-clé await
await attend qu'une Promise soit résolue et retourne le résultat :
async function getData() {
const result = await Promise.resolve('terminé');
console.log(result); // 'terminé'
}
Comparaison Promise vs async/await
Exemple 1 : Requête API simple
Écriture avec Promise :
function getUserData(userId) {
return fetch(`/api/users/${userId}`)
.then((response) => response.json())
.then((user) => {
console.log(user);
return user;
})
.catch((error) => {
console.error('Erreur :', error);
throw error;
});
}
Écriture avec async/await :
async function getUserData(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
const user = await response.json();
console.log(user);
return user;
} catch (error) {
console.error('Erreur :', error);
throw error;
}
}
Exemple 2 : Chaîner plusieurs opérations asynchrones
Écriture avec Promise :
function processUserData(userId) {
return fetchUser(userId)
.then((user) => {
return fetchPosts(user.id);
})
.then((posts) => {
return fetchComments(posts[0].id);
})
.then((comments) => {
console.log(comments);
return comments;
})
.catch((error) => {
console.error('Erreur :', error);
});
}
Écriture avec async/await :
async function processUserData(userId) {
try {
const user = await fetchUser(userId);
const posts = await fetchPosts(user.id);
const comments = await fetchComments(posts[0].id);
console.log(comments);
return comments;
} catch (error) {
console.error('Erreur :', error);
}
}
Gestion des erreurs
try/catch vs .catch()
async/await avec try/catch :
async function fetchData() {
try {
const response = await fetch('/api/data');
const data = await response.json();
return data;
} catch (error) {
console.error('Requête échouée :', error);
// On peut traiter différents types d'erreurs ici
if (error.name === 'NetworkError') {
// Traiter l'erreur réseau
}
throw error; // Relancer ou retourner une valeur par défaut
}
}
Utilisation mixte (non recommandé mais fonctionnel) :
async function fetchData() {
const response = await fetch('/api/data').catch((error) => {
console.error('Requête échouée :', error);
return null;
});
if (!response) return null;
const data = await response.json();
return data;
}
try/catch multiniveau
Pour les erreurs à différentes étapes, on peut utiliser plusieurs blocs try/catch :
async function complexOperation() {
let user;
try {
user = await fetchUser();
} catch (error) {
console.error('Échec de récupération de l\'utilisateur :', error);
return null;
}
try {
const posts = await fetchPosts(user.id);
return posts;
} catch (error) {
console.error('Échec de récupération des articles :', error);
return []; // Retourner un tableau vide comme valeur par défaut
}
}
Exemples d'application pratique
Exemple : Processus de correction d'examens
Flux : Corriger l'examen → Vérifier la récompense → Attribuer la récompense → Exclusion ou punition
// Corriger l'examen
function correctTest(name) {
return new Promise((resolve, reject) => {
setTimeout(() => {
const score = Math.round(Math.random() * 100);
if (score >= 60) {
resolve({
name,
score,
});
} else {
reject('Vous avez atteint le seuil d\'exclusion');
}
}, 2000);
});
}
// Vérifier la récompense
function checkReward(data) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (data.score >= 90) {
resolve(`${data.name} obtient des billets de cinéma`);
} else if (data.score >= 60 && data.score < 90) {
resolve(`${data.name} obtient une distinction`);
} else {
reject('Pas de récompense pour vous');
}
}, 2000);
});
}
Écriture avec Promise :
correctTest('John Doe')
.then((data) => checkReward(data))
.then((reward) => console.log(reward))
.catch((error) => console.log(error));
Réécriture avec async/await :
async function processStudent(name) {
try {
const data = await correctTest(name);
const reward = await checkReward(data);
console.log(reward);
return reward;
} catch (error) {
console.log(error);
return null;
}
}
processStudent('John Doe');
Exemple : Exécution concurrente de plusieurs requêtes
Quand plusieurs requêtes n'ont pas de dépendances entre elles, elles doivent être exécutées simultanément :
❌ Incorrect : Exécution séquentielle (plus lent) :
async function fetchAllData() {
const users = await fetchUsers(); // Attendre 1 seconde
const posts = await fetchPosts(); // Attendre encore 1 seconde
const comments = await fetchComments(); // Attendre encore 1 seconde
// Total 3 secondes
return { users, posts, comments };
}
✅ Correct : Exécution concurrente (plus rapide) :
async function fetchAllData() {
// Lancer trois requêtes simultanément
const [users, posts, comments] = await Promise.all([
fetchUsers(),
fetchPosts(),
fetchComments(),
]);
// Seulement 1 seconde nécessaire (le temps de la requête la plus lente)
return { users, posts, comments };
}
Utiliser Promise.allSettled() pour gérer les échecs partiels :
async function fetchAllData() {
const results = await Promise.allSettled([
fetchUsers(),
fetchPosts(),
fetchComments(),
]);
const users = results[0].status === 'fulfilled' ? results[0].value : [];
const posts = results[1].status === 'fulfilled' ? results[1].value : [];
const comments = results[2].status === 'fulfilled' ? results[2].value : [];
return { users, posts, comments };
}
Pièges courants
1. Utiliser await dans les boucles (exécution séquentielle)
❌ Incorrect : Attendre à chaque itération, inefficace :
async function processUsers(userIds) {
const results = [];
for (const id of userIds) {
const user = await fetchUser(id); // Exécution séquentielle, très lent !
results.push(user);
}
return results;
}
// S'il y a 10 utilisateurs et que chaque requête prend 1 seconde, cela prend 10 secondes au total
✅ Correct : Exécution concurrente avec Promise.all() :
async function processUsers(userIds) {
const promises = userIds.map((id) => fetchUser(id));
const results = await Promise.all(promises);
return results;
}
// 10 utilisateurs en requêtes concurrentes, seulement 1 seconde
Compromis : Limiter la concurrence :
async function processUsersWithLimit(userIds, limit = 3) {
const results = [];
for (let i = 0; i < userIds.length; i += limit) {
const batch = userIds.slice(i, i + limit);
const batchResults = await Promise.all(batch.map((id) => fetchUser(id)));
results.push(...batchResults);
}
return results;
}
// Traiter 3 à la fois, évitant d'envoyer trop de requêtes simultanément
2. Oublier d'utiliser await
Oublier await donne un Promise au lieu de la valeur réelle :
// ❌ Incorrect
async function getUser() {
const user = fetchUser(1); // await oublié, user est un Promise
console.log(user.name); // undefined (Promise n'a pas de propriété name)
}
// ✅ Correct
async function getUser() {
const user = await fetchUser(1);
console.log(user.name); // Nom correct
}
3. Utiliser await sans async
await ne peut être utilisé qu'à l'intérieur de fonctions async :
// ❌ Incorrect : Erreur de syntaxe
function getData() {
const data = await fetchData(); // SyntaxError
return data;
}
// ✅ Correct
async function getData() {
const data = await fetchData();
return data;
}
await de niveau supérieur (Top-level await) :
Dans ES2022 et les environnements de modules, on peut utiliser await au niveau supérieur d'un module :
// ES2022 module
const data = await fetchData(); // Utilisable au niveau supérieur du module
console.log(data);
4. Omission de la gestion des erreurs
Sans try/catch, les erreurs ne seront pas capturées :
// ❌ Peut entraîner des erreurs non capturées
async function fetchData() {
const response = await fetch('/api/data'); // En cas d'échec, une erreur sera lancée
return response.json();
}
// ✅ Ajouter la gestion des erreurs
async function fetchData() {
try {
const response = await fetch('/api/data');
return response.json();
} catch (error) {
console.error('Erreur :', error);
return null; // Ou retourner une valeur par défaut
}
}
5. Les fonctions async retournent toujours une Promise
Même sans utiliser await, une fonction async retourne une Promise :
async function getValue() {
return 42; // Retourne en réalité Promise.resolve(42)
}
// Il faut utiliser .then() ou await pour obtenir la valeur
getValue().then((value) => console.log(value)); // 42
// Ou
async function printValue() {
const value = await getValue();
console.log(value); // 42
}
Applications avancées
Gestion du timeout
Implémentation d'un mécanisme de timeout avec Promise.race() :
function timeout(ms) {
return new Promise((_, reject) => {
setTimeout(() => reject(new Error('Délai d\'attente dépassé')), ms);
});
}
async function fetchWithTimeout(url, ms = 5000) {
try {
const response = await Promise.race([fetch(url), timeout(ms)]);
return await response.json();
} catch (error) {
console.error('Requête échouée :', error.message);
throw error;
}
}
// Utilisation
fetchWithTimeout('/api/data', 3000); // Timeout de 3 secondes
Mécanisme de réessai
Implémentation d'un réessai automatique en cas d'échec :
async function fetchWithRetry(url, retries = 3, delay = 1000) {
for (let i = 0; i < retries; i++) {
try {
const response = await fetch(url);
return await response.json();
} catch (error) {
if (i === retries - 1) throw error; // Dernier essai échoué, lancer l'erreur
console.log(`Tentative ${i + 1} échouée, nouvel essai dans ${delay}ms...`);
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
}
// Utilisation
fetchWithRetry('/api/data', 3, 2000); // Maximum 3 tentatives, intervalle de 2 secondes
Traitement séquentiel avec conservation de l'état
Parfois il faut exécuter séquentiellement tout en conservant tous les résultats :
async function processInOrder(items) {
const results = [];
for (const item of items) {
const result = await processItem(item);
results.push(result);
// On peut décider de l'étape suivante en fonction du résultat précédent
if (result.shouldStop) {
break;
}
}
return results;
}
async/await dans l'Event Loop
async/await reste fondamentalement du Promise, et suit donc les mêmes règles de l'Event Loop :
console.log('1');
async function test() {
console.log('2');
await Promise.resolve();
console.log('3');
}
test();
console.log('4');
// Ordre de sortie : 1, 2, 4, 3
Analyse :
console.log('1')- Exécution synchronetest()est appelé,console.log('2')- Exécution synchroneawait Promise.resolve()- Le code suivant est placé dans la file des micro tasksconsole.log('4')- Exécution synchrone- Le micro task s'exécute,
console.log('3')
Points clés pour les entretiens
- async/await est du sucre syntaxique pour Promise : Plus lisible mais fondamentalement identique
- La gestion des erreurs utilise try/catch : Pas
.catch() - Attention concurrent vs séquentiel : Ne pas utiliser await aveuglément dans les boucles
- Les fonctions async retournent toujours un Promise : Même sans return Promise explicite
- await uniquement dans les fonctions async : Sauf top-level await (ES2022)
- Comprendre l'Event Loop : Le code après await est un micro task
Sujets connexes
- Promise - Base d'async/await
- Event Loop - Comprendre l'ordre d'exécution