⏱️ 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é.
let n = 0;
const id = setInterval(() => {
n += 1;
console.log("tic", n);
if (n === 3) {
clearInterval(id);
console.log("termine");
}
}, 20);
Affichagetic 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) => …).
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");
Affichagelance
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.
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");
Affichagemain 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).
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...");
Affichageen 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).
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
| Outil | Rô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.