Promesses et async/await en JavaScript
Callbacks, promesses, async/await : comprenez comment JavaScript gère les opérations asynchrones, avec fetch, gestion d’erreurs, exécution parallèle et annulation.

Callbacks, promesses, async/await : comprenez comment JavaScript gère les opérations asynchrones, avec fetch, gestion d’erreurs, exécution parallèle et annulation.

Charger des données depuis une API, lire un fichier, attendre une réponse utilisateur : toutes ces opérations prennent du temps, et JavaScript ne doit pas figer la page en attendant. C’est le rôle de l'asynchrone. Les promesses et la syntaxe async/await en sont aujourd’hui l’interface standard.
Cet article avance du problème vers la solution : pourquoi l’asynchrone existe, ce qu’est une promesse, comment écrire du code lisible avec async/await, et comment gérer erreurs, parallélisme et annulation.
JavaScript exécute votre code sur un seul fil principal dans le navigateur. Si une opération longue bloquait ce fil, l’interface serait gelée : plus de clic, plus de défilement fluide. Les opérations d’entrée-sortie (réseau, minuteries, fichiers côté Node.js) sont donc déléguées à l’environnement. Une fois terminées, leur résultat est replacé dans une file et traité quand le fil principal est disponible : c’est la boucle d’événements, présentée dans Comment fonctionne JavaScript dans le navigateur.
console.log('1 : début');
setTimeout(() => {
console.log('3 : après la minuterie');
}, 0);
console.log('2 : fin du script');
Même avec un délai de 0 ms, la fonction de rappel s’exécute après le code synchrone en cours. Ce comportement surprend au début, mais il est cohérent.
Historiquement, on passait une fonction à appeler une fois l’opération finie :
lireFichier('a.txt', (erreur, contenu) => {
if (erreur) return gererErreur(erreur);
lireFichier('b.txt', (erreur2, contenu2) => {
if (erreur2) return gererErreur(erreur2);
// ... et ainsi de suite : c'est la « pyramide »
});
});
Les imbrications s’empilent, la gestion d’erreurs se répète, et l’enchaînement devient difficile à lire.
Une promesse (Promise) représente le résultat futur d’une opération asynchrone. Elle est dans l’un de ces états :
Une promesse ne change d’état qu’une seule fois. La page MDN sur Promise détaille toutes ses méthodes.
const attendre = (ms) =>
new Promise((resolve) => setTimeout(resolve, ms));
attendre(500)
.then(() => console.log('500 ms plus tard'))
.catch((e) => console.error(e))
.finally(() => console.log('Terminé'));
.then() traite le succès, .catch() l’échec, .finally() s’exécute dans les deux cas. Chaque appel renvoie une nouvelle promesse, ce qui permet le chaînage.
Le mot-clé async devant une fonction la fait toujours renvoyer une promesse. À l’intérieur, await met la fonction en pause jusqu’à ce que la promesse soit réglée, sans bloquer le fil principal.
async function chargerUtilisateur(id) {
const reponse = await fetch(`/api/utilisateurs/${id}`);
const utilisateur = await reponse.json();
return utilisateur;
}
chargerUtilisateur(42).then((u) => console.log(u.nom));
C’est du sucre syntaxique sur les promesses : tout ce que vous savez sur elles reste valable. Voir la page MDN sur les fonctions async et sur l’opérateur await.
Deux points à garder en tête :
await ne s’utilise que dans une fonction async ou au niveau supérieur d’un module (voir Modules JavaScript : import, export et ESM) ;async ne « devient » pas synchrone : l’appelant récupère une promesse et doit lui aussi attendre ou chaîner.L’API fetch renvoie une promesse de réponse. Un piège classique : fetch ne rejette pas la promesse sur une réponse HTTP 404 ou 500. Elle n’est rompue que pour une erreur réseau. Il faut donc vérifier reponse.ok.
async function chargerJson(url) {
const reponse = await fetch(url);
if (!reponse.ok) {
throw new Error(`Erreur HTTP ${reponse.status}`);
}
return reponse.json();
}
Centraliser cette vérification dans une petite fonction utilitaire évite d’oublier le test dans dix fichiers.
Avec async/await, on utilise try/catch :
async function afficherProfil(id) {
const zone = document.querySelector('#profil');
try {
zone.textContent = 'Chargement…';
const profil = await chargerJson(`/api/utilisateurs/${id}`);
zone.textContent = profil.nom;
} catch (erreur) {
// On informe l'utilisateur et on garde une trace pour le débogage
zone.textContent = 'Impossible de charger le profil.';
console.error(erreur);
}
}
Quelques règles :
catch vide cache les bugs ;catch ni try/catch produit une erreur de type « rejet non géré », visible dans la console.Pour afficher le résultat dans la page, appuyez-vous sur DOM JavaScript : manipuler la page correctement.
Deux appels indépendants lancés l’un après l’autre perdent du temps :
// Séquentiel : le second appel attend la fin du premier
const utilisateur = await chargerJson('/api/utilisateur');
const commandes = await chargerJson('/api/commandes');
S’ils ne dépendent pas l’un de l’autre, lancez-les ensemble :
// Parallèle : les deux requêtes partent en même temps
const [utilisateur, commandes] = await Promise.all([
chargerJson('/api/utilisateur'),
chargerJson('/api/commandes'),
]);
Promise.all : toutes doivent réussir ; la première erreur rompt l’ensemble.Promise.allSettled : attend toutes les promesses et renvoie le statut de chacune, utile quand un échec partiel est acceptable.Promise.race : renvoie la première promesse réglée, réussie ou non.Promise.any : renvoie la première promesse tenue.Attention à ne pas lancer un nombre énorme de requêtes en même temps : le serveur, le navigateur ou l’API ont des limites. Pour de grandes listes, traitez par lots.
Quand l’utilisateur quitte un écran ou tape une nouvelle recherche, la requête précédente devient inutile. L’AbortController permet de l’annuler :
let controleur;
async function rechercher(terme) {
// On annule la recherche précédente encore en cours
controleur?.abort();
controleur = new AbortController();
try {
const reponse = await fetch(`/api/recherche?q=${encodeURIComponent(terme)}`, {
signal: controleur.signal,
});
return await reponse.json();
} catch (erreur) {
if (erreur.name === 'AbortError') return null; // annulation volontaire
throw erreur;
}
}
Cela évite aussi qu’une réponse ancienne n’écrase une réponse plus récente à l’écran, un bug subtil mais fréquent sur les champs de recherche.
await : vous manipulez une promesse au lieu de sa valeur ; vous verrez Promise { <pending> } dans la console.await dans une boucle forEach : forEach n’attend pas les fonctions async. Utilisez for...of pour le séquentiel ou Promise.all avec map pour le parallèle.reponse.ok avec fetch..then() et await sans raison, ce qui rend le code difficile à suivre.new Promise qui encapsule un fetch).chargerCommandes, envoyerFormulaire.role="status" ou aria-live, comme évoqué dans les ressources du pôle audit de Yunt.L’asynchrone n’est pas un mystère : une opération longue est déléguée, une promesse représente son résultat, et async/await permet d’écrire le tout de façon lisible. Retenez trois réflexes : vérifier reponse.ok, entourer d’un try/catch ce qui peut échouer, et lancer en parallèle ce qui est indépendant.
Pour mettre tout cela en pratique, construisez une petite page qui charge une liste depuis une API, gère le chargement et l’erreur, puis ajoutez une recherche annulable. Pour un parcours structuré, découvrez Academy Yunt et son blog.
Non. Le code JavaScript reste sur le fil principal. Ce sont les opérations d’entrée-sortie (réseau, minuteries) qui sont déléguées à l’environnement, puis rapportées via la boucle d’événements.
async/await à .then() ?Dans la plupart des cas, oui, pour la lisibilité. Les deux reposent sur les mêmes promesses, et .then() reste pratique pour des enchaînements très courts.
fetch ne lève-t-il pas d’erreur sur une réponse 404 ?Parce que la requête a abouti : le serveur a répondu. Seule une erreur réseau rompt la promesse. Testez reponse.ok ou reponse.status.
await en dehors d’une fonction ?Oui, au niveau supérieur d’un module ES, ce qui suppose d’utiliser type="module" dans le navigateur ou la configuration ESM sous Node.js.
Annulez la requête précédente avec AbortController, ou comparez un identifiant de requête avant d’afficher le résultat.