Leçon 4/6 11 min

Asynchrone : Promises et async/await

Maîtrisez l'asynchrone JS : Promises, async/await, try/catch et tâches parallèles avec Promise.all.

Pourquoi l'asynchrone ? JavaScript n'a qu'un fil

JavaScript est mono-thread : il n'exécute qu'une chose à la fois. Si une opération longue (un appel réseau, lire un fichier) bloquait ce fil, toute la page gèlerait : plus de clic, plus de scroll, plus rien. Inacceptable.

La solution : les opérations longues sont asynchrones. On les lance, le fil continue, et on est prévenu plus tard quand le résultat arrive. Historiquement, on passait une fonction callback à appeler à la fin :

// L'enfer des callbacks : illisible dès qu'on enchaîne
chargerUtilisateur(1, (user) => {
  chargerCommandes(user, (commandes) => {
    chargerDetails(commandes[0], (details) => {
      console.log(details); // 3 niveaux d'imbrication... et ça empire
    });
  });
});

Ça marche, mais ça devient vite illisible (le « callback hell »). Les Promises ont été créées pour remettre ça à plat.

Les Promises : une valeur qui arrivera plus tard

Une Promise est un objet qui représente un résultat pas encore disponible. Elle a trois états : pending (en attente), puis soit fulfilled (réussie, avec une valeur), soit rejected (échouée, avec une erreur). Une fois résolue, elle ne change plus.

Une Promise part de l'état pending, puis bascule définitivement soit en fulfilled (valeur, traitée par then) soit en rejected (erreur, traitée par catch). pending en attente fulfilled valeur → .then() rejected erreur → .catch() succès échec
Une Promise bascule une seule fois : de pending vers fulfilled ou rejected.
fetch("/api/user/1")          // renvoie une Promise
  .then(reponse => reponse.json())  // .then : quand c'est prêt
  .then(user => console.log(user.name))
  .catch(err => console.error("Échec :", err)); // .catch : si erreur

Chaque .then() renvoie une nouvelle Promise, ce qui permet de les chaîner à plat au lieu d'imbriquer. Mais on peut faire encore plus lisible.

async / await : du code asynchrone qui se lit comme du synchrone

async et await sont du sucre syntaxique par-dessus les Promises. Dans une fonction marquée async, await met en pause cette fonction (pas la page) jusqu'à ce que la Promise se résolve, et renvoie sa valeur :

async function afficherUtilisateur() {
  const reponse = await fetch("/api/user/1");
  const user = await reponse.json();
  console.log(user.name);   // se lit de haut en bas, comme du synchrone
}

Même logique que les .then() enchaînés, mais ça se lit comme une recette, ligne après ligne. C'est la forme préférée en JavaScript moderne.

Une fonction async renvoie toujours une Promise. Et await ne s'utilise qu'à l'intérieur d'une fonction async (ou au top-level d'un module).

Gérer les erreurs et plusieurs tâches

Avec await, les erreurs se gèrent avec un try / catch classique :

async function charger() {
  try {
    const r = await fetch("/api/user/1");
    if (!r.ok) throw new Error("HTTP " + r.status);
    return await r.json();
  } catch (err) {
    console.error("Échec du chargement :", err.message);
    return null;
  }
}

Et pour lancer plusieurs tâches en parallèle (au lieu d'attendre l'une après l'autre), Promise.all attend que toutes réussissent, Promise.allSettled attend la fin de toutes même si certaines échouent :

// Les 3 requêtes partent en même temps, on attend la dernière
const [a, b, c] = await Promise.all([
  fetch("/api/a"), fetch("/api/b"), fetch("/api/c"),
]);

Le piège : await en série dans une boucle. Faire for (const u of urls) { await fetch(u) } attend chaque requête l'une après l'autre (lent). Si elles sont indépendantes, lance-les ensemble avec Promise.all(urls.map(u => fetch(u))).

À toi de jouer

Ce code simule une requête avec une Promise et un setTimeout. Complète la fonction main avec await pour afficher le résultat, puis exécute :

function requeteSimulee() {
  return new Promise((resolve) => {
    setTimeout(() => resolve({ name: "Leanne" }), 300);
  });
}

async function main() {
  console.log("Chargement...");
  const user = await requeteSimulee();  // attend la Promise
  console.log("Reçu :", user.name);
}

main();

Why async? JavaScript has only one thread

JavaScript is single-threaded: it runs one thing at a time. If a long operation (a network call, reading a file) blocked that thread, the whole page would freeze: no clicks, no scroll, nothing. Unacceptable.

The solution: long operations are asynchronous. You start them, the thread keeps going, and you're notified later when the result arrives. Historically, you passed a callback to run at the end:

// Callback hell: unreadable as soon as you chain
loadUser(1, (user) => {
  loadOrders(user, (orders) => {
    loadDetails(orders[0], (details) => {
      console.log(details); // 3 levels of nesting... and it gets worse
    });
  });
});

It works, but it gets unreadable fast ("callback hell"). Promises were created to flatten this out.

Promises: a value that will arrive later

A Promise is an object representing a result that's not available yet. It has three states: pending, then either fulfilled (succeeded, with a value) or rejected (failed, with an error). Once settled, it never changes again.

A Promise starts pending, then settles permanently into either fulfilled (value, handled by then) or rejected (error, handled by catch). pending waiting fulfilled value → .then() rejected error → .catch() success failure
A Promise settles only once: from pending to fulfilled or rejected.
fetch("/api/user/1")          // returns a Promise
  .then(response => response.json())  // .then: when it's ready
  .then(user => console.log(user.name))
  .catch(err => console.error("Failed:", err)); // .catch: on error

Each .then() returns a new Promise, letting you chain them flat instead of nesting. But you can do even better.

async / await: async code that reads like sync

async and await are syntactic sugar over Promises. Inside a function marked async, await pauses that function (not the page) until the Promise settles, and returns its value:

async function showUser() {
  const response = await fetch("/api/user/1");
  const user = await response.json();
  console.log(user.name);   // reads top to bottom, like sync code
}

Same logic as chained .then(), but it reads like a recipe, line by line. It's the preferred form in modern JavaScript.

An async function always returns a Promise. And await is only used inside an async function (or at a module's top level).

Handling errors and multiple tasks

With await, errors are handled with a classic try / catch:

async function load() {
  try {
    const r = await fetch("/api/user/1");
    if (!r.ok) throw new Error("HTTP " + r.status);
    return await r.json();
  } catch (err) {
    console.error("Load failed:", err.message);
    return null;
  }
}

And to run several tasks in parallel (instead of one after another), Promise.all waits for all to succeed, Promise.allSettled waits for all to finish even if some fail:

// The 3 requests start at once, you wait for the last one
const [a, b, c] = await Promise.all([
  fetch("/api/a"), fetch("/api/b"), fetch("/api/c"),
]);

The trap: serial await in a loop. Writing for (const u of urls) { await fetch(u) } waits for each request one after another (slow). If they're independent, fire them together with Promise.all(urls.map(u => fetch(u))).

Your turn

This code simulates a request with a Promise and a setTimeout. Complete the main function with await to print the result, then run it:

function fakeRequest() {
  return new Promise((resolve) => {
    setTimeout(() => resolve({ name: "Leanne" }), 300);
  });
}

async function main() {
  console.log("Loading...");
  const user = await fakeRequest();  // wait for the Promise
  console.log("Got:", user.name);
}

main();

🎯 Pratique

S'entraîner (clique pour ouvrir) :

🧠 Rappel libre
Rappel libre

Sans remonter : que renvoie une fonction async, et pourquoi await ne gèle-t-il pas la page ?

Une fonction async renvoie toujours une Promise (la valeur retournée est automatiquement emballée dedans). await ne met en pause que la fonction async où il se trouve, pas le fil principal : pendant l'attente, le navigateur reste libre de réagir aux clics et de faire tourner le reste. C'est de l'asynchrone, pas du blocage.
⚖️ Juge le code de l'IA
Accepter ou rejeter le code de l'IA

Tu demandes à l'IA de récupérer 3 ressources indépendantes. Elle propose ce code. L'accepter ou le rejeter, et pourquoi ?

async function tout() {
  const a = await fetch("/api/a");
  const b = await fetch("/api/b");
  const c = await fetch("/api/c");
  return [a, b, c];
}
Rejeter (pour la performance). Le code est correct mais lent : les 3 requêtes sont indépendantes, or chaque await attend la fin de la précédente avant de lancer la suivante (séquentiel). Total = somme des 3 temps. Comme elles ne dépendent pas l'une de l'autre, il faut les lancer ensemble : const [a, b, c] = await Promise.all([...]). Total = le temps de la plus lente.
Que fait await dans une fonction async ?
Combien de fois une Promise peut-elle changer d'état une fois résolue ?
Tu as 5 requêtes indépendantes à faire le plus vite possible. Quoi de mieux ?
Prochaine étape

Tu sais gérer l'asynchrone proprement. On s'en sert tout de suite : les Web APIs du navigateur (géolocalisation, notifications, presse-papier) sont presque toutes asynchrones et renvoient des Promises.

Leçon 5 : Les Web APIs →

Une erreur dans cette leçon, un passage flou, une question ? Écrivez-moi : chaque retour améliore ce cours.

Besoin d'un développeur pour votre projet ?

Réponse sous 24h · Sans engagement