07 - async/await, le sucre syntaxique qui change tout
Ce que tu vas apprendre
- Ce que async/await fait vraiment sous le capot (spoiler : des Promises)
- Comment
awaitsuspend l'exécution et rend la main a l'event loop - Le try/catch pour gerer les erreurs async
- Le top-level await et ses implications
Prerequisites
Avoir lu l'article 06 - Promises.
La première fois que j'ai remplace une chaîne de .then() par async/await, j'ai eu l'impression de retirer des lunettes sales. Le meme code, mais lisible. Sauf que comprendre le sucre syntaxique sans comprendre ce qu'il cache, c'est conduire une voiture sans savoir qu'il y a un moteur.
async : une fonction qui retourne toujours une Promise
Quand tu mets async devant une fonction, deux choses se passent :
- La fonction retourne toujours une Promise
- Tu gagnes le droit d'utiliser
awaita l'intérieur
typescriptasync function greet(): Promise<string> {
return "Salut";
}
// Strictement equivalent a :
function greet(): Promise<string> {
return Promise.resolve("Salut");
}
Meme si tu retournes une valeur brute, elle est automatiquement wrappee dans Promise.resolve(). Et si tu lances une erreur, elle est wrappee dans Promise.reject() :
typescriptasync function fail(): Promise<never> {
throw new Error("boom");
}
// Equivalent a :
function fail(): Promise<never> {
return Promise.reject(new Error("boom"));
}
Ca veut dire que toute fonction async est chaînable avec .then(). Les deux mondes sont compatibles.
await : suspendre sans bloquer
Voici le truc que beaucoup de devs comprennent mal. await ne bloque pas le thread. Il suspend l'exécution de la fonction courante et rend la main a l'event loop. Le reste du programme continue de tourner.
typescriptasync function fetchUser(id: string) {
console.log("Debut fetch");
const response = await fetch(`/api/users/${id}`);
console.log("Response recue");
const user = await response.json();
console.log("User parse");
return user;
}
console.log("Avant appel");
fetchUser("42");
console.log("Apres appel");
L'ordre de la console :
Avant appel
Debut fetch
Apres appel // <-- l'event loop reprend la main ici
Response recue
User parse
"Apres appel" s'affiche avant "Response reçue". La fonction fetchUser est suspendue au premier await, et le code appelant continue. C'est exactement ce que faisait .then(), mais sans l'indentation infernale.
Ce que le compilateur fait vraiment
Sous le capot, le moteur JavaScript transforme ton async/await en machine a états basee sur des Promises. Voici une version simplifiee de la transformation :
typescript// Ce que tu ecris :
async function getPrice(id: string) {
const product = await fetchProduct(id);
const discount = await fetchDiscount(product.category);
return product.price * (1 - discount);
}
// Ce que le moteur execute (simplifie) :
function getPrice(id: string) {
return fetchProduct(id)
.then((product) => {
return fetchDiscount(product.category)
.then((discount) => {
return product.price * (1 - discount);
});
});
}
Chaque await devient un point de coupure. Le moteur enregistre l'état local (variables, position dans la fonction), créé un callback qui reprendra là où on s'est arrêté, et retourne le contrôle a l'event loop.
C'est pour ca que les variables declarees avant un await sont toujours accessibles apres : le moteur les conserve dans une closure implicite.
try/catch : la gestion d'erreurs naturelle
Le gros avantage d'async/await, c'est que tu retrouves le try/catch classique pour les erreurs asynchrones :
typescriptasync function loadUserProfile(id: string) {
try {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const user = await response.json();
return user;
} catch (error) {
console.error("Impossible de charger le profil :", error);
return null;
}
}
Compare avec la version Promises :
typescriptfunction loadUserProfile(id: string) {
return fetch(`/api/users/${id}`)
.then((response) => {
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
})
.catch((error) => {
console.error("Impossible de charger le profil :", error);
return null;
});
}
Le try/catch a un avantage discret mais precieux : il attrape les erreurs synchrones et asynchrones dans le meme bloc. Un .catch() sur une Promise ne capte pas une erreur synchrone lancee avant le return de la Promise.
Le piège classique : oublier await
typescriptasync function saveAndNotify(data: Record<string, unknown>) {
save(data); // Oups, pas de await
notify("saved"); // S'execute avant que save() finisse
}
Si save() retourne une Promise et que tu oublies await, la fonction continue immédiatement. Pas d'erreur, pas de warning. Le bug sera silencieux et intermittent -- le pire genre de bug.
TypeScript aide un peu avec la regle @typescript-eslint/no-floating-promises, qui détecté les Promises non gerees. Active-la. Serieusement.
Top-level await
Depuis ES2022 (et dans les modules ESM), tu peux utiliser await en dehors d'une fonction async :
typescript// config.ts (ESM module)
const response = await fetch("/api/config");
const config = await response.json();
export default config;
Le module entier devient une sorte de fonction async. Les modules qui importent config attendent que la Promise soit résolue avant de s'exécuter.
C'est pratique pour l'initialisation, mais attention : un top-level await bloque le chargement de tous les modules qui dependent de celui-ci. Sur un projet comme paltemps.fr, un top-level await dans un module utilitaire partage pourrait ralentir le démarrage de toute l'application.
Mon conseil : reserve le top-level await aux fichiers d'entree (main.ts, server.ts) et aux scripts ponctuels.
async/await vs .then() : quand utiliser quoi
La majorite du temps, async/await est plus lisible. Mais il y a des cas ou .then() reste pertinent :
typescript// .then() est plus concis pour un one-liner
const users = await fetch("/api/users").then((r) => r.json());
// .then() est utile pour du fire-and-forget (avec gestion d'erreur)
sendAnalytics(event).catch(console.error);
Mon heuristique : si tu as plus d'un await dans une fonction, utilise async/await. Pour un appel unique sans logique de branchement, .then() est acceptable.
Résumé
asyncdevant une fonction = elle retourne toujours une Promiseawaitsuspend la fonction et rend la main a l'event loop, il ne bloque pas le thread- Sous le capot, c'est une machine a états basee sur des Promises
- try/catch fonctionne pour les erreurs synchrones et asynchrones
- Oublier
awaitest un bug silencieux : activeno-floating-promises - Top-level await est puissant mais peut ralentir le chargement des modules
Article précédent : 06 - Promises Article suivant : 08 - Gestion des erreurs asynchrones