Retour au cours

frontend / javascript

Asynchrone : callbacks

Leçon 101 exercice

Explication

Ce que vous allez apprendre

  • Expliquer pourquoi JavaScript a besoin de callbacks pour gérer les opérations longues
  • Prédire l'ordre d'exécution entre code synchrone et callback asynchrone
  • Appliquer la convention Node.js "erreur en premier argument" (error-first callback)
  • Identifier une pyramide de callbacks ("callback hell") dans du code existant
  • Justifier pourquoi ce problème motive directement les Promises, vues à la leçon suivante

Dans quel contexte ?

Un développeur reprend un vieux module Node.js qui lit un fichier de configuration, puis interroge une base de données, puis envoie un e-mail de confirmation — trois callbacks imbriqués sur six niveaux d'indentation. Il doit ajouter une quatrième étape (journaliser l'action dans un fichier d'audit) et se rend compte qu'ajouter le moindre callback supplémentaire rend le code totalement illisible, et que la moindre erreur dans l'étape 2 doit être remontée manuellement à travers trois niveaux de fonctions. C'est exactement ce problème structurel que les Promises résoudront à la leçon suivante.

1. Un seul thread, donc un problème à résoudre

JavaScript exécute son code sur un seul thread : il ne peut faire qu'une chose à la fois. Pourtant, une page web doit régulièrement attendre quelque chose sans se figer — une réponse serveur, un minuteur, un fichier à lire.

2. La solution historique : le callback

Un callback est une fonction que l'on transmet à une opération, et que cette opération se charge d'appeler plus tard, une fois son travail terminé, sans bloquer le reste du programme entre-temps.

Prérequis

Cette leçon suppose que la notion de fonction passée en argument (vue à la leçon 2 sur les fonctions et closures) est bien acquise : un callback n'est rien d'autre qu'une fonction transmise à une autre fonction.

3. Un changement de mentalité à intégrer

Avec un callback, on ne sait pas exactement QUAND le code s'exécutera, seulement qu'il s'exécutera "après" un certain événement. Le script continue immédiatement après avoir lancé l'opération asynchrone.

4. Pourquoi l'ordre d'affichage surprend souvent

Cela explique un phénomène qui déroute beaucoup de débutants : le code synchrone s'affiche toujours avant, même s'il est écrit après le callback dans le fichier. L'ordre d'écriture ne correspond pas à l'ordre d'exécution.

5. Une convention pour gérer les erreurs

Dans l'écosystème Node.js, une convention s'est imposée : le callback reçoit toujours l'erreur éventuelle en premier argument (null si tout va bien), puis le résultat.

Argument du callbackSi succèsSi échec
1er argument (err)nullObjet Error
2e argument (resultat)La donnée obtenueundefined

Piège fréquent

Oublier de vérifier err en premier dans un callback (function(err, data) { console.log(data) }) laisse passer les erreurs silencieusement : la donnée sera undefined et le code plantera plus loin, loin de la cause réelle du problème.

6. Il reste un problème : la pyramide de callbacks

Cette convention est simple, mais elle devient vite un problème dès qu'il faut enchaîner plusieurs opérations asynchrones dépendantes les unes des autres : chaque étape s'imbrique dans la précédente, créant une pyramide de code difficile à lire, surnommée le "callback hell".

Cette leçon pose volontairement ce problème sans le résoudre : les deux prochains chapitres, Promises puis async/await, existent précisément pour y répondre.

Commandes & code

Asynchrone : callbacks

La brique de base de l'asynchrone en JS, et ses limites historiques.

js
// setTimeout : callback execute apres un delai minimum (pas garanti exact)
console.log("1. Debut");
setTimeout(() => {
    console.log("3. Callback execute apres le delai");
}, 1000);
console.log("2. Fin du script synchrone");
// Ordre affiche : 1, 2, 3 -- le script continue sans attendre le setTimeout

// Callback classique pour une operation asynchrone simulee
function chargerDonnees(id, callback) {
    setTimeout(() => {
        if (id <= 0) {
            callback(new Error("ID invalide"), null);       // convention Node : (erreur, resultat)
            return;
        }
        callback(null, { id, nom: `Element ${id}` });
    }, 500);
}

chargerDonnees(1, (erreur, donnees) => {
    if (erreur) {
        console.error("Erreur :", erreur.message);
        return;
    }
    console.log("Donnees recues :", donnees);
});

// --- Le "callback hell" : le probleme que Promise/async-await resolvent ---
function etape1(callback) {
    setTimeout(() => callback(null, "resultat1"), 100);
}
function etape2(donnee, callback) {
    setTimeout(() => callback(null, donnee + "-resultat2"), 100);
}
function etape3(donnee, callback) {
    setTimeout(() => callback(null, donnee + "-resultat3"), 100);
}

// Pyramide de callbacks imbriques : difficile a lire, a maintenir, gestion d'erreur repetitive
etape1((err1, res1) => {
    if (err1) return console.error(err1);
    etape2(res1, (err2, res2) => {
        if (err2) return console.error(err2);
        etape3(res2, (err3, res3) => {
            if (err3) return console.error(err3);
            console.log("Resultat final :", res3);
        });
    });
});
// Ce pattern (nesting croissant + gestion d'erreur dupliquee) motive directement les Promises

// --- Callbacks synchrones vs asynchrones : une distinction cruciale ---
function forEachSync(tableau, callback) {
    for (const item of tableau) {
        callback(item);            // execute IMMEDIATEMENT, dans le meme tick
    }
}
forEachSync([1, 2, 3], (n) => console.log("sync :", n));

function forEachAsync(tableau, callback) {
    tableau.forEach((item) => {
        setTimeout(() => callback(item), 0);    // execute au tick SUIVANT (macrotask)
    });
}
forEachAsync([1, 2, 3], (n) => console.log("async :", n));
console.log("Ce message s'affiche AVANT les callbacks asynchrones ci-dessus");

// --- Pattern courant : callback d'erreur "error-first" (convention Node.js historique) ---
const fs = require("fs");    // (exemple Node.js)
fs.readFile("fichier.txt", "utf8", (erreur, contenu) => {
    if (erreur) {
        console.error("Impossible de lire le fichier :", erreur.message);
        return;
    }
    console.log(contenu);
});

// --- Event listeners : des callbacks appeles potentiellement PLUSIEURS fois ---
document.addEventListener("click", (event) => {
    console.log("clic detecte");     // callback invoque a chaque clic, indefiniment
});

Résumé

  • Un callback asynchrone (setTimeout, I/O) s'exécute plus tard, sans bloquer le script principal.
  • La convention Node.js "error-first" (erreur, resultat) => {} structure la gestion d'erreur.
  • L'imbrication croissante de callbacks ("callback hell") motive directement l'existence des Promises.
  • Un callback peut être appelé zéro, une, ou plusieurs fois (event listeners) : bien distinguer ces cas.

Exercices pratiques

1 disponible
1

Mission : sauver le module de configuration à six niveaux

Objectif : Tracer l'ordre d'exécution d'un code à callbacks, corriger une erreur non vérifiée, et aplatir une pyramide de callbacks imbriqués.

Contexte

Technologik maintient un vieux module Node.js qui lit un fichier de config, interroge la base de données, puis envoie un e-mail de confirmation, avec trois callbacks imbriqués. Un développeur doit ajouter une quatrième étape (journaliser dans un fichier d'audit) mais un bug existant complique tout : dans l'étape 2, l'erreur potentielle n'est jamais vérifiée.

js
function etape1(cb) { setTimeout(() => cb(null, "config"), 100); }
function etape2(donnee, cb) { setTimeout(() => cb(new Error("DB indisponible"), null), 100); }
function etape3(donnee, cb) { setTimeout(() => cb(null, donnee + "-mail-envoye"), 100); }

etape1((err1, res1) => {
  etape2(res1, (err2, res2) => {
    etape3(res2, (err3, res3) => {
      console.log("Resultat final :", res3);
    });
  });
});

Tu dois comprendre pourquoi ce code plante silencieusement, le corriger, puis expliquer la limite structurelle de ce style.

Résoudre l’exercice →