Instructions asynchrones
Table des matières
L'asynchrone en JavaScript
Pourquoi l'asynchrone existe-t-il?
JavaScript est mono-thread : il ne peut exécuter qu'une seule instruction à la fois. Or, certaines opérations prennent du temps — charger un fichier, appeler une API, lire une base de données. Si JavaScript attendait chaque opération avant de continuer, la page serait figée pendant ce temps.
L'asynchrone permet de lancer une opération longue et continuer à exécuter le reste du code pendant que cette opération se déroule en arrière-plan. Quand elle se termine, JavaScript reprend pour traiter le résultat.
Synchrone (bloquant) Asynchrone (non-bloquant)
────────────────── ──────────────────────────
1. Instruction A 1. Instruction A
2. Attendre... 2. Lancer opération longue → part en arrière-plan
3. Attendre... 3. Instruction B ← continue sans attendre
4. Résultat reçu 4. Instruction C
5. Instruction B 5. Résultat reçu → traiter
Les 3 générations de l'asynchrone en JavaScript
Génération 1 — Les callbacks (à éviter aujourd'hui)
La première approche : passer une fonction à appeler quand l'opération se termine.
// Exemple avec setTimeout (simuler une attente)
console.log("Début");
setTimeout(function () {
console.log("Résultat après 2 secondes");
}, 2000);
console.log("Fin");
// Affiche dans cet ordre :
// Début
// Fin
// Résultat après 2 secondes ← arrive après !
Le problème : le callback hell
Quand les opérations s'enchaînent, le code devient illisible :
// Cauchemar de maintenance
chargerUtilisateur(id, function (user) {
chargerCommandes(user, function (commandes) {
chargerProduits(commandes, function (produits) {
calculerTotal(produits, function (total) {
afficher(total, function () {
// ...
});
});
});
});
});
Génération 2 — Les Promises
Une Promise (promesse) représente une valeur qui sera disponible dans le futur. Elle a trois états possibles :
- pending (en attente) : l'opération est en cours
- fulfilled (résolue) : l'opération a réussi, la valeur est disponible
- rejected (rejetée) : l'opération a échoué, une erreur est disponible
// Une Promise ressemble à ça
const promesse = new Promise((resolve, reject) => {
// Si tout va bien :
resolve("La valeur");
// Si ça échoue :
reject(new Error("Quelque chose a mal tourné"));
});
Chaîner les Promises avec .then() et .catch()
fetch("https://api.example.com/users/1")
.then((response) => response.json()) // quand la réponse arrive, la convertir en JSON
.then((user) => console.log(user.name)) // quand le JSON est prêt, afficher le nom
.catch((erreur) => console.error(erreur)); // si n'importe quelle étape échoue
C'est mieux que les callbacks, mais le chaînage de .then() reste verbeux pour des cas complexes.
Génération 3 — async / await (syntaxe moderne, à utiliser)
async / await est du sucre syntaxique par-dessus les Promises. Cela permet d'écrire du code asynchrone qui ressemble à du code synchrone, sans callbacks ni .then().
// Avec .then() (verbose)
function chargerUtilisateur(id) {
return fetch(`/api/users/${id}`)
.then((response) => response.json())
.then((user) => user.name);
}
// Avec async/await (lisible)
async function chargerUtilisateur(id) {
const response = await fetch(`/api/users/${id}`);
const user = await response.json();
return user.name;
}
awaitne bloque pas tout JavaScript — il suspend seulement la fonction courante et laisse le reste du programme continuer.
Le mot-clé async
async se place devant une fonction pour indiquer qu'elle est asynchrone.
// Fonction classique
async function maFonction() { ... }
// Fonction fléchée
const maFonction = async () => { ... }
// Méthode dans un objet
const obj = {
async maMethode() { ... }
}
Une fonction async retourne toujours une Promise, même si vous ne le voyez pas explicitement :
async function getMessage() {
return "Bonjour";
}
// Équivalent à :
function getMessage() {
return Promise.resolve("Bonjour");
}
// Pour lire la valeur :
getMessage().then((msg) => console.log(msg)); // 'Bonjour'
// ou
const msg = await getMessage(); // 'Bonjour' (dans un contexte async)
Le mot-clé await
await se place devant une Promise pour attendre son résultat avant de continuer.
async function exemple() {
console.log("1 - Avant");
const resultat = await unePromesse(); // attendre ici
console.log("2 - Après"); // s'exécute seulement quand la promesse est résolue
console.log(resultat);
}
Règles importantes :
awaitne peut s'utiliser qu'à l'intérieur d'une fonctionasyncawaitpeut précéder n'importe quelle Promise, pas seulementfetch
// INTERDIT — await en dehors d'une fonction async
function maFonction() {
const data = await fetch('/api') // SyntaxError
}
// CORRECT
async function maFonction() {
const data = await fetch('/api')
}
Gestion des erreurs avec try / catch
Avec les callbacks et les Promises, on utilisait .catch(). Avec async/await, on utilise le traditionnel try/catch :
async function chargerUtilisateur(id) {
try {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new Error(`Erreur HTTP : ${response.status}`);
}
const user = await response.json();
return user;
} catch (erreur) {
console.error("Impossible de charger l'utilisateur :", erreur.message);
return null;
}
}
Cas classiques capturés par catch :
- Pas de connexion réseau
- Serveur qui retourne une erreur (404, 500...)
- Réponse qui n'est pas du JSON valide
- Timeout
Exemples concrets
Appel API avec fetch
async function getUtilisateur(id) {
try {
const response = await fetch(
`https://jsonplaceholder.typicode.com/users/${id}`,
);
const user = await response.json();
return user;
} catch (erreur) {
console.error("Erreur :", erreur);
}
}
// Utilisation
const user = await getUtilisateur(1);
console.log(user.name); // 'Leanne Graham'
Opérations enchaînées
async function commanderProduit(userId, produitId) {
// Étape 1 — vérifier l'utilisateur
const user = await getUtilisateur(userId);
if (!user) throw new Error("Utilisateur introuvable");
// Étape 2 — vérifier le stock
const produit = await getProduit(produitId);
if (produit.stock === 0) throw new Error("Produit épuisé");
// Étape 3 — créer la commande
const commande = await creerCommande(user.id, produit.id);
// Étape 4 — envoyer la confirmation
await envoyerEmail(user.email, commande.id);
return commande;
}
Sans async/await, ces 4 étapes enchaînées auraient nécessité 4 .then() imbriqués ou une pyramide de callbacks.
Opérations en parallèle avec Promise.all
Par défaut, les await s'exécutent l'un après l'autre. Si les opérations sont indépendantes, lancez-les en parallèle pour gagner du temps :
// Séquentiel — chaque fetch attend le précédent
async function chargerDonneesSequentiel() {
const users = await fetch("/api/users").then((r) => r.json()); // attend
const produits = await fetch("/api/produits").then((r) => r.json()); // attend
const commandes = await fetch("/api/commandes").then((r) => r.json()); // attend
// Temps total ≈ t(users) + t(produits) + t(commandes)
}
// Parallèle — les trois fetches partent en même temps
async function chargerDonneesParallele() {
const [users, produits, commandes] = await Promise.all([
fetch("/api/users").then((r) => r.json()),
fetch("/api/produits").then((r) => r.json()),
fetch("/api/commandes").then((r) => r.json()),
]);
// Temps total ≈ max(t(users), t(produits), t(commandes))
}
Règle : utilisez
Promise.allquand les opérations ne dépendent pas les unes des autres.
Promise.allSettled — continuer même si une échoue
Promise.all s'arrête dès qu'une Promise échoue. Promise.allSettled attend toutes les Promises, qu'elles réussissent ou non :
const resultats = await Promise.allSettled([
fetch("/api/users").then((r) => r.json()),
fetch("/api/inexistant").then((r) => r.json()), // va échouer
fetch("/api/produits").then((r) => r.json()),
]);
resultats.forEach((resultat) => {
if (resultat.status === "fulfilled") {
console.log("Succès :", resultat.value);
} else {
console.log("Échec :", resultat.reason);
}
});
L'asynchrone dans Vue.js
Dans un composant — onMounted
import { ref, onMounted } from "vue";
const user = ref(null);
const isLoading = ref(true);
const erreur = ref(null);
onMounted(async () => {
try {
const response = await fetch("/api/users/1");
user.value = await response.json();
} catch (e) {
erreur.value = "Impossible de charger l'utilisateur";
} finally {
isLoading.value = false;
}
});
<template>
<div v-if="isLoading">Chargement...</div>
<div v-else-if="erreur">{{ erreur }}</div>
<div v-else>{{ user.name }}</div>
</template>
Dans un store Pinia - action asynchrone
import { defineStore } from "pinia";
import { ref } from "vue";
export const useUserStore = defineStore("user", () => {
const user = ref(null);
const isLoading = ref(false);
async function charger(id) {
isLoading.value = true;
try {
const response = await fetch(`/api/users/${id}`);
user.value = await response.json();
} finally {
isLoading.value = false;
}
}
return { user, isLoading, charger };
});
Erreurs classiques à éviter
1. Oublier await — la valeur est une Promise, pas le résultat
async function exemple() {
const data = fetch("/api/users"); // oubli de await
console.log(data); // Promise { <pending> } — pas les données !
const data2 = await fetch("/api/users"); //
console.log(data2); // Response { ... }
}
2. await dans une boucle forEach — ça ne fonctionne pas
// forEach n'attend pas les await
const ids = [1, 2, 3];
ids.forEach(async (id) => {
const user = await getUtilisateur(id); // ignoré par forEach !
console.log(user.name);
});
// Utiliser for...of
for (const id of ids) {
const user = await getUtilisateur(id);
console.log(user.name);
}
// Ou Promise.all pour la parallélisation
const users = await Promise.all(ids.map((id) => getUtilisateur(id)));
3. Ne pas gérer les erreurs
// Si fetch échoue, l'erreur est silencieuse
async function charger() {
const data = await fetch("/api/data").then((r) => r.json());
afficher(data);
}
// Toujours un try/catch
async function charger() {
try {
const data = await fetch("/api/data").then((r) => r.json());
afficher(data);
} catch (e) {
afficherErreur(e.message);
}
}
4. Async/await et le top-level (hors fonction)
await ne peut pas s'utiliser directement à la racine d'un fichier .js classique. Dans les modules ES (.mjs ou type="module"), c'est possible depuis 2022 :
// Top-level await (modules ES seulement)
const data = await fetch("/api").then((r) => r.json());
Résumé
| Concept | Ce que ça fait |
|---|---|
async function |
Déclare une fonction asynchrone qui retourne une Promise |
await |
Attend le résultat d'une Promise avant de continuer |
try / catch |
Capture les erreurs d'un bloc asynchrone |
Promise.all([...]) |
Lance plusieurs Promises en parallèle, attend toutes |
Promise.allSettled([...]) |
Comme all, mais continue même si une échoue |
for...of + await |
Boucle asynchrone séquentielle |
.map() + Promise.all |
Boucle asynchrone parallèle |
En une image mentale
Imaginez un restaurant :
- JavaScript = un seul serveur
- Opération asynchrone = une commande envoyée en cuisine
await= le serveur note la commande, va servir d'autres tables, et revient chercher le plat quand la cuisine l'appelle- Sans asynchrone = le serveur resterait planté devant la cuisine à attendre — personne d'autre ne serait servi
L'asynchrone permet au serveur (JavaScript) de rester productif pendant que les opérations longues (cuisine = réseau, fichiers, base de données) se déroulent en parallèle.
Atelier asynchrone (en devoir)
Prérequis
Écouter la vidéo suivante :
Fichiers associés exemple-asynchrone.zip
Consignes
À partir de lab03-depart.zip :
- Exécuter l'application, sans le backend, pour voir le résultat dans le navigateur. (npm run dev)
- Démarrer le backend et ensuite démarrer l'application pour voir ce qu'elle fait. (npm run backend)
- Exécuter les requêtes qui se trouvent dans le fichier
requests.httpafin de comprendre ce que retourne l'API REST. - Prendre connaissance du code du composant
dogsComponent.js. - Prendre connaissance des tests existants dans
dogsService.test.jsetdogsComponent.test.js - Écrire le code du test manquant dans
dogsService.test.js. - Écrire le code des deux tests manquant dans
dogsComponent.test.js.
Où est le serveur backend ?
Le backend, est le serveur Json-Server qui s'exécute localement. Ce serveur s'installe comme librairie de développement du projet et se démarre avec un script (voir package.json). C'est un serveur qui permet d'avoir rapidement un API REST pour le développement et qui fournit des données provenant d'un fichier json (voir dossier backend).
Remise
Aucune remise, mais conserver le lab03 il sera d'une grande utilité pour le TP02!
L'asynchrone dans les tests Vitest
Pourquoi l'asynchrone existe dans les tests?
Quand vous interagissez avec un composant Vue dans un test (cliquer un bouton, modifier un champ), Vue ne met pas à jour le DOM instantanément. Il attend le prochain cycle de rendu (appelé "tick") avant d'appliquer les changements.
Si votre assertion s'exécute avant ce cycle, le DOM n'est pas encore mis à jour — le test échoue, même si le composant fonctionne parfaitement.
// INCORRECT — l'assertion s'exécute trop tôt
wrapper.find("button").trigger("click");
expect(wrapper.text()).toContain("1"); // Le DOM n'est pas encore mis à jour !
// CORRECT — on attend la mise à jour avant de vérifier
await wrapper.find("button").trigger("click");
expect(wrapper.text()).toContain("1"); // Le DOM est à jour
La règle de base
Toujours
awaitles interactions avec le DOM dans un test Vue.
Cela implique deux choses :
- Ajouter
awaitdevanttrigger(),setValue(),setProps(), etc. - Déclarer le test avec
asyncpour pouvoir utiliserawaità l'intérieur.
// La signature d'un test asynchrone
it("description du test", async () => {
// ... le test avec des await
});
Les méthodes qui nécessitent await
trigger() — simuler un événement
it("incrémente le compteur au clic", async () => {
const wrapper = mount(Counter);
await wrapper.find("button").trigger("click");
expect(wrapper.find('[data-testid="count"]').text()).toBe("1");
});
setValue() — modifier la valeur d'un champ de formulaire
it("filtre la liste selon la saisie", async () => {
const wrapper = mount(SearchBox);
await wrapper.find("input").setValue("Alice");
expect(wrapper.text()).toContain("Alice");
expect(wrapper.text()).not.toContain("Bob");
});
setProps() — changer les props après le montage
it("met à jour l'affichage quand la prop change", async () => {
const wrapper = mount(WelcomeMessage, {
props: { name: "Alice" },
});
expect(wrapper.text()).toContain("Alice");
await wrapper.setProps({ name: "Bob" });
expect(wrapper.text()).toContain("Bob");
});
nextTick() — attendre manuellement un cycle de rendu
Parfois, une action interne au composant déclenche une mise à jour sans que vous interagissiez directement. Dans ce cas, importez nextTick de Vue :
import { nextTick } from "vue";
it("met à jour après une action interne", async () => {
const wrapper = mount(MonComposant);
// Déclencher une action interne (ex: via le store)
wrapper.vm.incrementer();
// Attendre le prochain cycle de rendu
await nextTick();
expect(wrapper.text()).toContain("1");
});
Les erreurs classiques
Oublier async sur le test
// await dans un test non-async — erreur de syntaxe
it('test incorrect', () => {
await wrapper.find('button').trigger('click') // SyntaxError
})
//
it('test correct', async () => {
await wrapper.find('button').trigger('click')
})
Oublier await devant trigger()
it("test qui échoue pour de mauvaises raisons", async () => {
const wrapper = mount(Counter);
wrapper.find("button").trigger("click"); // Pas de await !
// Le DOM n'est pas encore mis à jour — le test échoue
expect(wrapper.find('[data-testid="count"]').text()).toBe("1");
});
Ce type d'erreur est particulièrement difficile à déboguer : le test échoue, mais le composant fonctionne bien dans le navigateur. La cause est presque toujours un await manquant.
Plusieurs interactions dans un test
Chaque interaction doit avoir son propre await :
it("ajoute deux éléments à la liste", async () => {
const wrapper = mount(TodoList);
await wrapper.find("input").setValue("Tâche 1");
await wrapper.find('button[type="submit"]').trigger("click");
await wrapper.find("input").setValue("Tâche 2");
await wrapper.find('button[type="submit"]').trigger("click");
expect(wrapper.findAll("li")).toHaveLength(2);
});
Tester des appels API (fetch / axios)
Quand un composant fait un appel réseau, il ne faut pas faire de vrai appel dans les tests — c'est lent, fragile, et dépendant du réseau. On utilise vi.fn() pour simuler la réponse.
Mocker fetch avec vi.fn()
import { describe, it, expect, vi, beforeEach } from "vitest";
import { mount } from "@vue/test-utils";
import UserCard from "./UserCard.vue";
describe("UserCard", () => {
beforeEach(() => {
// Remplacer fetch par une fausse fonction
global.fetch = vi.fn();
});
it("affiche le nom de l'utilisateur après chargement", async () => {
// Simuler une réponse réseau réussie
global.fetch.mockResolvedValue({
json: () =>
Promise.resolve({ name: "Alice", email: "alice@example.com" }),
});
const wrapper = mount(UserCard, {
props: { userId: 1 },
});
// Attendre que le composant finisse de charger
await nextTick();
await nextTick(); // parfois deux ticks sont nécessaires (fetch + rendu)
expect(wrapper.text()).toContain("Alice");
});
it("affiche un message d'erreur si le chargement échoue", async () => {
// Simuler une erreur réseau
global.fetch.mockRejectedValue(new Error("Réseau indisponible"));
const wrapper = mount(UserCard, {
props: { userId: 1 },
});
await nextTick();
await nextTick();
expect(wrapper.text()).toContain("Erreur");
});
});
Mocker un module entier avec vi.mock()
Si votre composant importe un service (api.ts, userService.ts, etc.), vous pouvez remplacer tout le module :
import { vi } from "vitest";
// Ce mock remplace le fichier entier pour tous les tests du fichier
vi.mock("@/services/userService", () => ({
getUser: vi
.fn()
.mockResolvedValue({ name: "Alice", email: "alice@example.com" }),
}));
import { getUser } from "@/services/userService";
it("appelle le service et affiche le résultat", async () => {
const wrapper = mount(UserCard, { props: { userId: 1 } });
await nextTick();
expect(getUser).toHaveBeenCalledWith(1);
expect(wrapper.text()).toContain("Alice");
});
Tester les emits asynchrones
Quand un composant émet un événement à la suite d'une interaction, vérifiez avec wrapper.emitted() après l'await :
it('émet l\'événement "submit" avec les bonnes données', async () => {
const wrapper = mount(ContactForm);
await wrapper.find('input[name="email"]').setValue("test@example.com");
await wrapper.find("form").trigger("submit");
// emitted() retourne un objet : { nomEvenement: [[args1], [args2], ...] }
expect(wrapper.emitted("submit")).toBeTruthy();
expect(wrapper.emitted("submit")[0]).toEqual([
{ email: "test@example.com" },
]);
});
Rappel sur la structure de
emitted():
wrapper.emitted('submit')→ tableau de tous les appels à cet événementwrapper.emitted('submit')[0]→ arguments du premier appel (tableau)wrapper.emitted('submit')[0][0]→ premier argument du premier appel
Résumé visuel
| Situation | Solution |
|---|---|
| Cliquer un bouton | await trigger('click') |
| Modifier un input | await setValue('valeur') |
| Changer une prop | await setProps({ prop: valeur }) |
| Action interne au composant | await nextTick() |
| Appel fetch/axios | vi.fn().mockResolvedValue(...) + await nextTick() |
| Module entier à mocker | vi.mock('@/services/...') |
| Tester un emit | wrapper.emitted('nom')[0] après le await |
Récapitulatif - liste de vérification
Avant de soumettre vos tests, vérifiez :
- Chaque test qui utilise
awaitest déclaréasync - Chaque
trigger(),setValue(),setProps()est précédé deawait - Les vérifications (
expect) sont après tous lesawait - Les appels API sont mockés (pas de vrai réseau dans les tests)
wrapper.emitted()est utilisé après l'interaction, pas avant