Aller au contenu principal

async et await

Écrire du code asynchrone qui se lit de haut en bas, avec les try / catch que vous connaissez déjà.

Objectifs de la séance​

  • écrire une fonction async et utiliser await ;
  • gérer les erreurs avec try / catch / finally ;
  • savoir que async renvoie toujours une promesse ;
  • lancer des tâches indépendantes en parallèle avec Promise.all ;
  • utiliser Promise.allSettled quand un échec ne doit pas tout arrêter ;
  • reconnaître le schéma d'une requête fetch réelle.

Petite vérification​

Avant de commencer, vérifiez si vous savez...

  1. exécuter promesses.js et dire quelle ligne s'affiche en dernier
  2. dire ce qu'il faut ajouter à la fin d'une chaîne .then(...) pour ne jamais avoir d'« unhandled rejection »
  3. dire à quoi sert le mot return devant chargerQuestion(question.id + 1) dans un then
📌 Réponse attendue

Notions théoriques​

Le même code, en plus lisible​

Dans ce cours, chargerQuestion(id) est une fonction qui renvoie une promesse (celle de la séance précédente). Avec .then :

chargerQuestion(1).then((question) => {
console.log(question.enonce);
});

Avec async / await :

async function afficherQuestion(id) {
const question = await chargerQuestion(id); // on « attend » le résultat
console.log(question.enonce);
}

afficherQuestion(1);
  • async devant function annonce : « cette fonction contient des attentes » ;
  • await devant une promesse signifie : « mets cette fonction en pause jusqu'à ce que la promesse soit tenue, et donne-moi son résultat ».
Pause de la fonction, pas du programme

await met en pause uniquement la fonction async. Le reste du programme (et l'écran de l'application) continue de fonctionner normalement pendant l'attente.

Les règles à connaître​

  1. await ne s'écrit que dans une fonction async ;
  2. une fonction async renvoie toujours une promesse, même si vous écrivez return 5 ;
  3. pour utiliser le résultat d'une fonction async, il faut donc await (dans une autre fonction async) ou .then.
async function obtenirCinq() {
return 5;
}

console.log(obtenirCinq()); // Promise { 5 } (et non 5 !)
Le piège numéro 1 : oublier await
const question = chargerQuestion(1); // oubli du await
console.log(question.enonce); // undefined : question est une promesse, pas la question

Si vous voyez Promise { <pending> } ou undefined là où vous attendiez une valeur, c'est presque toujours un await oublié.

Les erreurs : try / catch / finally​

Une promesse rompue devient une erreur à l'endroit du await. On l'attrape avec le try / catch habituel.

async function afficherQuestion(id) {
try {
const question = await chargerQuestion(id);
console.log("Reçu :", question.enonce);
} catch (erreur) {
const message = erreur instanceof Error ? erreur.message : "Erreur inconnue";
console.log("Échec :", message);
} finally {
console.log("Chargement terminé");
}
}

afficherQuestion(99);
Échec : Question 99 introuvable
Chargement terminé

Le schéma complet d'un chargement​

Voici le modèle que vous reproduirez dans toutes vos applications : un indicateur de chargement, un try pour le succès, un catch pour l'échec, un finally pour remettre l'indicateur à zéro.

let enCours = false;

async function charger(id) {
enCours = true;
try {
const question = await chargerQuestion(id);
return question;
} catch (erreur) {
console.error("Chargement impossible :", erreur);
return null;
} finally {
enCours = false; // exécuté dans tous les cas
}
}

Plusieurs tâches : une par une, ou en parallèle ?​

Chaque await attend la fin de la tâche avant de passer à la suivante. Pour trois chargements indépendants de 300 ms, c'est inutilement long :

// (ces exemples sont écrits à l'intérieur d'une fonction async)

// Séquentiel : environ 900 ms
const q1 = await chargerQuestion(1);
const q2 = await chargerQuestion(2);
const q3 = await chargerQuestion(3);

Avec Promise.all, on les lance tous en même temps, puis on attend qu'ils soient tous finis : environ 300 ms.

const [q1, q2, q3] = await Promise.all([
chargerQuestion(1),
chargerQuestion(2),
chargerQuestion(3),
]);

Retrouvez la destructuration de tableau : Promise.all renvoie un tableau avec les résultats dans le même ordre que les promesses.

Promise.all échoue dès le premier échec

Si l'une des promesses est rompue, Promise.all est rompue aussi et vous perdez les autres résultats. Quand un échec partiel est acceptable (par exemple, afficher les questions qui ont pu être chargées), utilisez Promise.allSettled.

Promise.allSettled : tout attendre, même les échecs​

Promise.allSettled ne rompt jamais : il renvoie, pour chaque promesse, un objet qui indique si elle a réussi ("fulfilled") ou échoué ("rejected").

// (dans une fonction async)
const resultats = await Promise.allSettled([
chargerQuestion(1),
chargerQuestion(99), // échoue
chargerQuestion(3),
]);

const questions = resultats
.filter((resultat) => resultat.status === "fulfilled")
.map((resultat) => resultat.value);

console.log(questions.length); // 2

Deux pièges avec les boucles​

// PIÈGE : forEach n'attend pas les await de son callback
ids.forEach(async (id) => {
const question = await chargerQuestion(id);
console.log(question.enonce);
});
console.log("Terminé ?"); // s'affiche AVANT les questions !

Deux bonnes solutions :

// (dans une fonction async)
// 1) for...of : les chargements se font l'un après l'autre
for (const id of ids) {
const question = await chargerQuestion(id);
console.log(question.enonce);
}

// 2) map + Promise.all : les chargements se font en parallèle
const questions = await Promise.all(ids.map((id) => chargerQuestion(id)));

Une vraie requête réseau avec fetch​

fetch récupère des données sur Internet. Il renvoie une promesse, donc on utilise await. Le schéma est toujours le même : vérifier response.ok, puis lire le JSON.

async function chargerQuestions() {
const reponse = await fetch("https://exemple.fr/api/questions");

if (!reponse.ok) {
// fetch ne lève PAS d'erreur pour une réponse 404 ou 500 : c'est à vous de tester
throw new Error(`Le serveur a répondu ${reponse.status}`);
}

const donnees = await reponse.json(); // lire le JSON est aussi asynchrone
return donnees;
}
fetch ne signale pas les erreurs HTTP

fetch ne rompt sa promesse que si le réseau est coupé. Une réponse 404 ou 500 est considérée comme « tenue » : sans le test reponse.ok, votre code utiliserait une page d'erreur comme si c'étaient des données.


Test de mémorisation/compréhension​


Où le mot await est-il autorisé ?


Que renvoie une fonction async qui se termine par return 5 ?


Que contient la variable q après : const q = chargerQuestion(1); (sans await) ?


Pourquoi utiliser Promise.all plutôt que trois await successifs pour trois chargements indépendants ?


Que fait fetch lorsque le serveur répond 404 ?


Quel est le problème de ids.forEach(async (id) => { await chargerQuestion(id); }) ?



TP pour réfléchir et résoudre des problèmes​

Votre mission : charger des questions de plusieurs façons, avec gestion d'erreur. Travaillez dans le fichier labo-js/async.js, qui commence par les deux fonctions de la séance précédente :

function attendre(millisecondes) {
return new Promise((resolve) => setTimeout(resolve, millisecondes));
}

function chargerQuestion(id) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (id < 1 || id > 3) {
reject(new Error(`Question ${id} introuvable`));
return;
}
resolve({ id, enonce: `Énoncé de la question ${id}` });
}, 300);
});
}

Étape 1 — Une fonction async​

Écrivez afficherQuestion(id), qui attend la question puis affiche son énoncé.


Bonne pratique - Lire de haut en bas

Chaque ligne s'exécute dans l'ordre où elle est écrite : la lecture est naturelle, sans .then imbriqué.

Étape 2 — Gérer l'échec et nettoyer​

Entourez l'attente d'un try / catch / finally. Le finally affiche Chargement terminé.


Bonne pratique - Ne jamais appeler une fonction async sans traiter l'échec

Appelez afficherQuestion(99) pour tester : l'erreur est attrapée à l'intérieur de la fonction. Une fonction async sans try/catch renverrait une promesse rompue que personne n'attrape.

Étape 3 — Charger en parallèle​

Chargez les questions 1, 2 et 3 en même temps, puis comptez-les.


Bonne pratique - Parallèle seulement si les tâches sont indépendantes

Si la tâche B a besoin du résultat de la tâche A, il faut await A avant de lancer B. Sinon, Promise.all fait gagner un temps précieux.

Étape 4 — Tolérer un échec partiel​

Chargez les questions 1, 99 et 3, et gardez seulement celles qui ont réussi.


Bonne pratique - Penser à l'échec partiel

Un écran qui affiche 2 questions sur 3 vaut mieux qu'un écran d'erreur. Choisissez Promise.all (tout ou rien) ou Promise.allSettled (au mieux) en fonction du besoin.

📌 Une solution

Ce qu'il faut retenir​

NotionRésumé
async functionFonction qui peut contenir des await et qui renvoie toujours une promesse.
awaitMet la fonction en pause jusqu'au résultat de la promesse (le reste du programme continue).
try / catch / finallyGère l'échec (catch) et remet les indicateurs à zéro (finally).
Promise.allLance des tâches indépendantes en parallèle ; échoue dès le premier échec.
Promise.allSettledAttend tout et renvoie le statut de chacune : idéal pour tolérer un échec partiel.
fetchToujours tester response.ok avant de lire le JSON.
Piègesawait oublié, forEach avec await, fonction async sans catch.

Aperçu de la prochaine séance​

Votre code asynchrone est lisible et gère les erreurs. Il reste le plus difficile : les situations imprévues. Que faire si l'utilisateur double-clique, si les réponses arrivent dans le désordre, ou si deux tâches écrasent la même donnée ? Dans la prochaine séance, vous allez provoquer ces bugs, puis les corriger.