Essayer de coder ← Retour aux sujets

Asynchrone : ne pas bloquer

async

Certaines opérations durent (réseau, minuterie, fichier). Plutôt que de bloquer la page, JavaScript les lance et continue : le résultat arrive plus tard, par un callback, une promesse ou await.

setTimeout(fn, 1000)
Appelle fn dans une seconde (une seule fois)
setInterval(fn, 1000)
Appelle fn toutes les secondes, jusqu'à clearInterval
fetch(url)
Requête réseau : renvoie une promesse
promesse.then(f).catch(g)
Exécute f si elle réussit, g si elle échoue
async function f() { … }
Fonction qui renvoie toujours une promesse
const x = await promesse;
Attend le résultat (dans une fonction async)
ordre.js
console.log("A : debut");

setTimeout(() => console.log("C : minuterie à 0 ms"), 0);
Promise.resolve().then(() => console.log("B : promesse"));

console.log("D : fin du script");
Affichage
A : debut
D : fin du script
B : promesse
C : minuterie à 0 ms

fetch et async / await

fetch

fetch interroge un serveur ; avec await, le code asynchrone se lit comme du code ordinaire, et try / catch gère les échecs.

const rep = await fetch(url);
Attend la réponse HTTP
if (!rep.ok) throw new Error(…);
Une erreur 404 ou 500 ne lève pas d'exception toute seule
const donnees = await rep.json();
Lit le corps de la réponse et le convertit depuis JSON
await Promise.all([a, b])
Attend plusieurs promesses lancées en parallèle
try { … } catch (erreur) { … }
Gère l'échec réseau ou le JSON invalide
await new Promise((ok) => setTimeout(ok, 500))
Une pause de 500 ms
utilisateur.js
async function chargerUtilisateur(id) {
  try {
    const reponse = await fetch(`https://exemple.fr/api/utilisateurs/${id}`);
    if (!reponse.ok) {
      throw new Error(`Erreur HTTP ${reponse.status}`);
    }
    const utilisateur = await reponse.json();
    console.log(utilisateur.nom);
  } catch (erreur) {
    console.log("Chargement impossible :", erreur.message);
  }
}

chargerUtilisateur(42);

⏱️ Le cours : attendre sans bloquer

JavaScript exécute le code dans un seul fil : une instruction à la fois. Si une opération longue (une requête réseau de deux secondes) était synchrone, toute la page serait figée pendant ce temps : plus de clic, plus de défilement. Les opérations longues sont donc asynchrones : on les lance, on continue, et on est prévenu quand le résultat est prêt.

La boucle d'événements

1. Le code synchrone s'exécute jusqu'au bout (pile d'appels).
2. Puis toutes les microtâches : les .then() et le code après un await.
3. Puis UNE tâche de la file : une minuterie échue, un clic, une réponse réseau…
   et on recommence en 2.

C'est pourquoi, dans l'exemple de la première carte, « D » s'affiche avant « B » et « C » alors qu'il est écrit après : le code synchrone passe toujours en premier, les promesses ensuite, les minuteries ensuite, même avec un délai de 0 ms.

setTimeout et setInterval

setTimeout(fonction, ms) exécute la fonction une fois, après au moins ms millisecondes ; setInterval(fonction, ms) la répète jusqu'à ce qu'on l'arrête avec clearInterval(identifiant). Les deux renvoient un identifiant. Ces minuteries ne sont pas précises : la fonction attend aussi que le code en cours soit terminé.

intervalle.js
let n = 0;
const id = setInterval(() => {
  n += 1;
  console.log("tic", n);
  if (n === 3) {
    clearInterval(id);
    console.log("termine");
  }
}, 20);
Affichage
tic 1
tic 2
tic 3
termine

Callbacks et pyramide de la mort

Historiquement, on enchaînait des opérations asynchrones en passant une fonction à rappeler (*callback*) à chacune. Quand chaque étape dépend de la précédente, les fonctions s'emboîtent de plus en plus profondément et la gestion d'erreur devient illisible. Les promesses ont été créées pour aplatir ce code.

Les promesses

Une promesse (Promise) représente un résultat qui n'est pas encore là. Elle est *en attente* (pending), puis *tenue* (fulfilled, avec une valeur) ou *rompue* (rejected, avec une erreur). .then(f) enregistre ce qu'il faut faire en cas de succès et renvoie une nouvelle promesse, ce qui permet de chaîner ; .catch(g) intercepte un échec n'importe où dans la chaîne ; .finally(h) s'exécute dans tous les cas. On en crée avec new Promise((resolve, reject) => …).

promesses.js
function attendre(ms, valeur) {
  return new Promise((resolve) => setTimeout(() => resolve(valeur), ms));
}

attendre(30, "un")
  .then((v) => {
    console.log(v);
    return attendre(30, "deux");
  })
  .then((v) => console.log(v))
  .finally(() => console.log("fini"));
console.log("lance");
Affichage
lance
un
deux
fini

async / await : écrire comme en synchrone

Une fonction déclarée async renvoie toujours une promesse. À l'intérieur, await suspend la fonction (et seulement elle : la page reste réactive) jusqu'à ce que la promesse soit réglée, puis donne sa valeur. Le code s'écrit ligne à ligne, comme du code ordinaire. Deux await à la suite s'exécutent l'un après l'autre ; pour lancer plusieurs opérations indépendantes en même temps, on les rassemble avec Promise.all.

attente.js
function attendre(ms, valeur) {
  return new Promise((resolve) => setTimeout(() => resolve(valeur), ms));
}

async function main() {
  const a = await attendre(30, 1);
  const b = await attendre(30, 2);
  console.log("sequentiel :", a + b);

  const [c, d] = await Promise.all([attendre(30, 10), attendre(30, 20)]);
  console.log("parallele :", c + d);
}

main();
console.log("main lancee, le script continue");
Affichage
main lancee, le script continue
sequentiel : 3
parallele : 30

Gérer les erreurs

Avec await, on utilise l'ordinaire try / catch / finally. Une promesse rompue qu'on n'intercepte pas provoque une erreur « Uncaught (in promise) » dans la console : pense toujours au cas d'échec (réseau coupé, serveur en panne, JSON invalide).

erreurs.js
function echoue() {
  return new Promise((_, reject) => setTimeout(() => reject(new Error("réseau coupé")), 20));
}

async function essayer() {
  try {
    await echoue();
  } catch (erreur) {
    console.log("capturé :", erreur.message);
  } finally {
    console.log("nettoyage");
  }
}

essayer();
console.log("en attente...");
Affichage
en attente...
capturé : réseau coupé
nettoyage

fetch : interroger un serveur

fetch(url) renvoie une promesse de réponse. Deux pièges : (1) elle n'est rompue que si le réseau échoue — une réponse 404 ou 500 est une réponse « réussie » techniquement, d'où le test if (!reponse.ok) ; (2) lire le corps est lui aussi asynchrone : await reponse.json(). Pour envoyer des données, on précise la méthode, l'en-tête et le corps (voir Objets et JSON).

envoi.js
async function envoyer() {
  const reponse = await fetch("/api/scores", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ nom: "Lina", score: 42 }),
  });
  return reponse.ok;
}

Un navigateur n'autorise une requête vers un autre site que si celui-ci l'accepte (règle CORS) ; c'est une protection, pas une erreur de ton code.

Plusieurs opérations à la fois

OutilRôle
Promise.all([a, b])attend toutes les promesses ; échoue dès que l'une échoue
Promise.allSettled([a, b])attend toutes et décrit le sort de chacune (succès ou échec)
Promise.race([a, b])prend la première réglée (utile pour un délai maximal)

Dans une page : charger puis afficher

Le schéma classique : afficher « Chargement… », lancer la requête, remplacer par le résultat ou par un message d'erreur, dans un try / catch. Sans cela, l'utilisateur croit que la page est cassée. Les éléments d'interface se mettent à jour avec le DOM.

⚠️ Erreurs fréquentes

Oublier le await

À éviter
faux.js
const reponse = fetch(url);          // une PROMESSE, pas la réponse
const donnees = reponse.json();      // TypeError : reponse.json is not a function
Correct
correct.js
const reponse = await fetch(url);
const donnees = await reponse.json();

await en dehors d'une fonction async

await n'est autorisé que dans une fonction async (ou au niveau principal d'un module <script type="module">). Ailleurs : « SyntaxError: await is only valid in async functions ». Déclare la fonction async.

Croire que setTimeout(fn, 0) s'exécute tout de suite

La fonction attend la fin du code synchrone et des promesses en cours. Zéro milliseconde ne veut pas dire « maintenant », mais « dès que possible, après le reste ».

Attendre en série ce qui peut se faire en parallèle

À éviter
faux.js
const a = await charger("a");     // 1 s
const b = await charger("b");     // 1 s de plus : 2 s en tout
Correct
correct.js
const [a, b] = await Promise.all([charger("a"), charger("b")]);   // 1 s en tout

async dans un forEach

liste.forEach(async (x) => { await traiter(x); }) lance tous les traitements sans les attendre : le code qui suit ne sait pas quand ils finissent. Utilise for (const x of liste) { await traiter(x); } (en série) ou await Promise.all(liste.map(traiter)) (en parallèle).

Ne pas tester reponse.ok

Une page 404 renvoie du HTML : await reponse.json() plante ensuite avec « Unexpected token < ». Teste reponse.ok avant de lire.

Oublier d'arrêter un setInterval

Un setInterval continue à tourner (et à consommer de la mémoire) tant qu'on ne l'arrête pas, même après que l'utilisateur a quitté la vue concernée. Garde l'identifiant et appelle clearInterval.

Bloquer le fil avec une boucle

Une boucle while qui « attend » une condition fige la page entière, car rien d'autre ne peut s'exécuter pendant ce temps. Attends avec une minuterie ou await, jamais avec une boucle active.

📋 Aide-mémoire

  • Ordre d'exécution : code synchrone, puis promesses (.then, après await), puis minuteries et événements.
  • setTimeout(fn, ms) une fois, setInterval(fn, ms) en boucle (+ clearInterval), fn sans parenthèses.
  • Promesse : new Promise((resolve, reject) => …), .then(…), .catch(…), .finally(…).
  • async function renvoie une promesse ; await seulement dedans ; try / catch pour les erreurs.
  • fetch : await fetch(url), tester reponse.ok, puis await reponse.json() ; ne rejette pas sur un 404.
  • En parallèle : Promise.all ; jamais de forEach avec async ; jamais de boucle active pour attendre.