Javascript 2
Exercice 1 — La roulette russe
function russianRoulette() {
if (Math.random() < 0.5) {
throw new Error("PAN");
}
}
try {
russianRoulette();
// On n'arrive ici que si aucune exception n'a été levée
console.log("You survived :D");
} catch (e) {
console.log("You're dead :(");
}
L’exception interrompt la fonction comme un return. Si personne ne l’attrape,
elle remonte toute la pile — et le programme s’arrête.
Exercice 2 — Première promesse
const URL =
"https://raw.githubusercontent.com/JulienUsson/zz3-f2-js/refs/heads/master/api/users.json";
console.log("avant");
fetch(URL)
.then((response) => response.json())
.then((users) => {
const frenchUsers = users.filter((user) => user.location.country === "France");
console.log("Utilisateurs en France :", frenchUsers);
})
.catch((error) => console.error("Erreur :", error));
console.log("après");
avant et après s’affichent tous les deux avant la liste : fetch rend la
main immédiatement, le .then() s’exécutera plus tard.
ℹ️ Avec axios, la réponse est déjà
décodée : axios.get(URL).then((response) => response.data).
Exercice 3 — Emballer un callback
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function joke() {
console.log("- Toc toc");
await delay(500);
console.log("- Qui est là ?");
await delay(10000);
console.log("- C'est Internet Explorer");
}
joke();
setTimeout est plus vieux que les promesses : on l’emballe une fois, et on ne
revoit plus jamais son callback.
Exercice 4 — Le même, en async/await
const URL =
"https://raw.githubusercontent.com/JulienUsson/zz3-f2-js/refs/heads/master/api/users.json";
async function showFrenchUsers() {
const response = await fetch(URL);
const users = await response.json();
console.log(users.filter((user) => user.location.country === "France"));
}
showFrenchUsers();
Le code se lit de haut en bas, mais il est exactement aussi asynchrone que la
version précédente : await ne bloque que la fonction où il se trouve.
Exercice 5 — Devinez l’ordre
A
C
F
D
E
B
- A — code synchrone, exécuté immédiatement.
- C — le corps d’une fonction
asyncs’exécute synchronement jusqu’au premierawait.go()n’est pas « mise de côté » : elle démarre tout de suite. - F — au premier
await,go()rend la main. Le reste du fichier finit de s’exécuter avant la suite dego(). - D puis E — la pile est vide, on vide la file des microtâches, dans
l’ordre où elles ont été empilées : celle du
await nulld’abord, celle du.then()ensuite. - B — les macrotâches en dernier.
setTimeout(..., 0)ne veut pas dire « maintenant », mais « dès qu’il n’y a plus rien d’autre à faire ».
Le point le plus souvent raté est C avant F : async ne rend pas une
fonction « parallèle », il lui donne juste le droit de faire des pauses.
Exercice 6 — La chaîne cassée
Le mot manquant est return :
function versionThen() {
return new Promise((resolve, reject) => {
fetchUser()
.then((user) => {
return fetchPosts(user).then((posts) => resolve(posts))
// ^^^^^^
})
.catch(reject)
})
}
Sans ce return, la promesse rendue par fetchPosts(user) n’est raccrochée à
aucune chaîne. Son rejet n’atteint donc jamais le .catch(reject) : la
promesse extérieure reste pending pour toujours, et l’erreur ressort en
unhandledRejection.
Avec le return, .then() attend la promesse retournée et propage son rejet
au maillon suivant — c’est-à-dire au .catch().
C’est le bug numéro un des promesses en vrai code : une requête qui ne
répond jamais, sans message d’erreur. La règle : dans un .then(), on retourne
toujours la promesse qu’on démarre.
D’ailleurs, ici le new Promise(...) est inutile — fetchUser() en est déjà
une. La version honnête tient en deux lignes :
const versionThen = () => fetchUser().then((user) => fetchPosts(user))
Exercice 7 — Quand la requête échoue
async function loadUsers() {
try {
const response = await fetch(URL);
if (!response.ok) throw new Error(response.status);
console.log("Users loaded");
} catch (error) {
console.log("Something went wrong");
} finally {
console.log("Request finished");
}
}
⚠️ fetch ne rejette pas sur un 404 ou un 500 : il faut tester
response.ok soi-même. C’est le piège classique.
La même chose avec les promesses :
fetch(URL)
.then((response) => {
if (!response.ok) throw new Error(response.status);
console.log("Users loaded");
})
.catch(() => console.log("Something went wrong"))
.finally(() => console.log("Request finished"));
Exercice 8 — Deux requêtes en parallèle
const BASE =
"https://raw.githubusercontent.com/JulienUsson/zz3-f2-js/refs/heads/master/api";
async function countAllUsers() {
console.time("parallèle");
const [users, users2] = await Promise.all([
fetch(`${BASE}/users.json`).then((r) => r.json()),
fetch(`${BASE}/users2.json`).then((r) => r.json()),
]);
console.timeEnd("parallèle");
console.log(`Total : ${[...users, ...users2].length}`);
}
Écrit avec deux await à la suite, on paie la somme des deux temps ; avec
Promise.all, le plus long des deux.
Exercice 9 — Abandonner au bout de N secondes
function fetchWithTimeout(promise, ms) {
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error("Timeout")), ms),
);
return Promise.race([promise, timeout]);
}
La promesse perdante n’est pas annulée : elle continue jusqu’au bout, son résultat est simplement ignoré. Pour vraiment interrompre une requête, il faut un AbortController.
Exercice 10 — Compter les succès et les échecs
async function report() {
const results = await Promise.allSettled([
Promise.resolve(1),
Promise.reject(new Error("boom")),
Promise.resolve(3),
]);
const success = results.filter((r) => r.status === "fulfilled").length;
const failed = results.filter((r) => r.status === "rejected").length;
console.log(`Succès : ${success}, Échecs : ${failed}`);
}
Avec Promise.all, le premier échec rejette tout : on perd les deux succès.
Exercice 11 — Réessayer en cas d’échec
async function retry(fn, attempts) {
for (let attempt = 1; attempt <= attempts; attempt++) {
try {
return await fn();
} catch (error) {
// La dernière tentative a échoué : on laisse l'erreur remonter
if (attempt === attempts) throw error;
}
}
}
La boucle se lit mieux que la récursion, et le nombre de tentatives restantes n’a pas besoin d’être transporté d’appel en appel.
Exercice 12 — Espacer les tentatives
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function retry(fn, attempts) {
for (let attempt = 1; attempt <= attempts; attempt++) {
try {
return await fn();
} catch (error) {
if (attempt === attempts) throw error;
// 1re attente 1 s, puis 2 s, puis 4 s…
await delay(2 ** (attempt - 1) * 1000);
}
}
}
Pourquoi doubler ? Si le serveur est saturé, le marteler toutes les secondes aggrave son cas. Le backoff exponentiel laisse au service le temps de repartir, et répartit les clients qui réessaient tous en même temps.
Exercice 13 — Limiter la concurrence
async function parallelLimit(tasks, limit) {
const results = [];
const running = new Set();
for (const [index, task] of tasks.entries()) {
const promise = task().then((value) => {
running.delete(promise);
return value;
});
results[index] = promise;
running.add(promise);
// On n'avance que quand une place se libère
if (running.size >= limit) {
await Promise.race(running);
}
}
return Promise.all(results);
}
Trois idées :
results[index]garde l’ordre d’entrée, alors que les tâches finissent dans le désordre ;runningest l’ensemble des tâches en vol, et chacune s’en retire elle-même ;Promise.race(running)est ce qui fait attendre juste ce qu’il faut : la première place libre.
⚠️ Tel quel, une tâche qui échoue reste dans running. À vous de voir comment
la retirer aussi dans ce cas — .finally() est votre ami.