async/await et les promesses en JavaScript
Dès qu’un programme JavaScript doit attendre — une requête réseau, la lecture d’un fichier, un délai — il devient asynchrone. Le couple async/await est aujourd’hui la façon la plus lisible d’écrire ce type de code, mais il repose entièrement sur les promesses. Comprendre les deux, et savoir passer de l’un à l’autre, est indispensable pour tout développeur web. Ce guide déroule chaque opération : créer une promesse, la consommer, gérer les erreurs, paralléliser, appeler une API et éviter les pièges classiques.
Pourquoi l’asynchrone ?
JavaScript est mono-thread : il n’exécute qu’une chose à la fois. Si une opération lente bloquait le fil d’exécution, toute la page se figerait. L’asynchrone permet de lancer une tâche longue, de continuer le reste du code, et de traiter le résultat quand il arrive.
console.log("1");
setTimeout(() => console.log("2 (plus tard)"), 0);
console.log("3");
// Affiche : 1, 3, 2 — le setTimeout est différé
Qu’est-ce qu’une promesse ?
Une promesse (Promise) représente une valeur qui n’est pas encore disponible. Elle a trois états : pending (en attente), fulfilled (résolue) ou rejected (rejetée). On la crée avec un exécuteur recevant deux fonctions, resolve et reject.
const promesse = new Promise((resolve, reject) => {
const succes = true;
setTimeout(() => {
if (succes) {
resolve("Données reçues");
} else {
reject(new Error("Échec du chargement"));
}
}, 1000);
});
Consommer une promesse : then, catch, finally
On réagit au résultat avec .then() (succès), .catch() (erreur) et .finally() (toujours exécuté).
promesse
.then((valeur) => {
console.log("Succès :", valeur);
})
.catch((erreur) => {
console.error("Erreur :", erreur.message);
})
.finally(() => {
console.log("Terminé, quel que soit le résultat");
});
.then() renvoie elle-même une promesse, ce qui permet de chaîner des opérations séquentielles. La valeur retournée dans un .then est transmise au suivant.
recupererUser(1)
.then((user) => recupererCommandes(user.id)) // renvoie une promesse
.then((commandes) => console.log(commandes.length))
.catch((err) => console.error(err));
Un seul .catch() en fin de chaîne intercepte l’erreur de n’importe quel maillon.
La syntaxe async/await
async/await est du sucre syntaxique sur les promesses. Une fonction async renvoie toujours une promesse, et await met en pause l’exécution jusqu’à ce que la promesse soit résolue — sans bloquer le reste du programme.
async function afficherUser() {
const user = await recupererUser(1); // attend la résolution
const commandes = await recupererCommandes(user.id);
console.log(commandes.length);
}
Le même enchaînement qu’avec .then(), mais qui se lit comme du code synchrone, de haut en bas. C’est là tout l’intérêt. await ne s’utilise qu’à l’intérieur d’une fonction async (ou au niveau supérieur d’un module).
Gérer les erreurs avec try/catch
Avec async/await, on remplace .catch() par un bloc try/catch classique, ce qui unifie la gestion des erreurs synchrones et asynchrones.
async function chargerDonnees() {
try {
const user = await recupererUser(1);
const commandes = await recupererCommandes(user.id);
return commandes;
} catch (erreur) {
console.error("Impossible de charger :", erreur.message);
return []; // valeur de repli
} finally {
console.log("Fin de chargement");
}
}
Paralléliser avec Promise.all
await en série attend chaque promesse l’une après l’autre. Quand les opérations sont indépendantes, lancez-les en parallèle avec Promise.all, qui attend que toutes soient résolues.
async function chargerTout() {
// En parallèle : les trois partent en même temps
const [users, produits, stats] = await Promise.all([
fetch("/api/users").then(r => r.json()),
fetch("/api/produits").then(r => r.json()),
fetch("/api/stats").then(r => r.json()),
]);
return { users, produits, stats };
}
Attention : Promise.all rejette dès qu’une promesse échoue. Si vous voulez le résultat de chacune, succès ou échec, utilisez Promise.allSettled.
const resultats = await Promise.allSettled([
recupererUser(1),
recupererUser(999), // échoue peut-être
]);
// [{ status: "fulfilled", value: ... }, { status: "rejected", reason: ... }]
Promise.race et Promise.any
Promise.race renvoie le résultat de la première promesse terminée, qu’elle réussisse ou échoue. Pratique pour imposer un délai maximal.
function timeout(ms) {
return new Promise((_, reject) =>
setTimeout(() => reject(new Error("Trop long")), ms)
);
}
async function avecDelai() {
// Échoue si la requête dépasse 3 secondes
return Promise.race([fetch("/api/lent"), timeout(3000)]);
}
Promise.any, lui, renvoie la première promesse réussie et ignore les échecs tant qu’il en reste une qui peut aboutir.
Récupérer des données avec fetch
fetch est l’API standard pour les requêtes HTTP, et elle est fondée sur les promesses. Point important : fetch ne rejette pas sur une erreur HTTP (404, 500) ; il faut vérifier response.ok soi-même.
async function getUser(id) {
const reponse = await fetch(`https://api.exemple.com/users/${id}`);
if (!reponse.ok) {
throw new Error(`HTTP ${reponse.status}`);
}
return reponse.json(); // renvoie une promesse
}
// POST avec un corps JSON
async function creerUser(data) {
const reponse = await fetch("https://api.exemple.com/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
return reponse.json();
}
Pour aller plus loin sur les verbes HTTP et les codes de statut, consultez notre guide des API REST.
Le piège de l’await en boucle
C’est l’erreur de performance la plus fréquente. Un await dans une boucle for traite les éléments un par un, en série, ce qui est lent quand les opérations sont indépendantes.
// Lent : chaque requête attend la précédente
async function chargerLent(ids) {
const resultats = [];
for (const id of ids) {
const user = await getUser(id); // séquentiel
resultats.push(user);
}
return resultats;
}
La bonne approche : lancer toutes les requêtes en parallèle avec map, puis attendre l’ensemble avec Promise.all.
// Rapide : toutes les requêtes partent en même temps
async function chargerRapide(ids) {
const promesses = ids.map((id) => getUser(id));
return Promise.all(promesses);
}
Ce patron combine les méthodes de tableau et les promesses. Réservez l’await en boucle aux cas où l’ordre compte réellement (par exemple des écritures dépendantes).
Autres pièges courants
Quelques erreurs à connaître pour ne pas se faire piéger.
// 1. Oublier await : on manipule une promesse, pas la valeur
async function bug() {
const user = getUser(1); // Promise, pas l'objet
console.log(user.nom); // undefined
}
// 2. Oublier return dans une chaîne .then
promesse.then((v) => {
traiter(v); // sans return, le .then suivant reçoit undefined
});
// 3. Avaler les erreurs silencieusement
async function silencieux() {
try {
await risque();
} catch {
// ne rien faire ici masque le vrai problème
}
}
À retenir
Une promesse représente une valeur future ; .then/.catch/.finally la consomment, et async/await la rend lisible comme du code synchrone. Gérez les erreurs avec try/catch, parallélisez les tâches indépendantes avec Promise.all plutôt que d’enchaîner les await, et vérifiez toujours response.ok après un fetch. Le réflexe le plus rentable : dès que vous écrivez await dans une boucle, demandez-vous si Promise.all ne ferait pas le travail plus vite.