Avez-vous une question? (216) 97 656 803 hajjriadh@gmail.com
JavaScript

18.3 Les callbacks asynchrones et le « callback hell »

Les callbacks asynchrones et le « callback hell ». Dans cette leçon du cours JavaScript, vous apprendrez à : • définir un callback et distinguer callback synchrone et asynchrone ; • écrire une fonctio...

0 chapitres 0 QCM Accès gratuit
Dans cet article

À propos de ce cours

Les callbacks asynchrones et le « callback hell ». Dans cette leçon du cours JavaScript, vous apprendrez à : • définir un callback et distinguer callback synchrone et asynchrone ; • écrire une fonction asynchrone qui accepte un callback ; • appliquer la convention « error-first » pour gérer les erreurs ; • reconnaître le « callback hell » et ses inconvénients.

Chapitre 18 — La programmation asynchrone · Leçon 3 : Les callbacks asynchrones et le « callback hell »

🎯 Objectifs de la leçon
• définir un callback et distinguer callback synchrone et asynchrone ;
• écrire une fonction asynchrone qui accepte un callback ;
• appliquer la convention « error-first » pour gérer les erreurs ;
• reconnaître le « callback hell » et ses inconvénients.

1. Qu'est-ce qu'un callback ?

Un callback (fonction de rappel) est une fonction passée en argument à une autre fonction, qui l'appellera au moment voulu. En JavaScript, les fonctions sont des valeurs comme les autres : on peut les stocker dans une variable ou les passer en paramètre.

Un callback n'est pas forcément asynchrone. Ceux de map, filter ou forEach sont synchrones:

// Callback SYNCHRONE : appelé immédiatement, pendant l'exécution de map()
const doubles = [1, 2, 3].map((x) => x * 2);
console.log(doubles);

// Notre propre fonction qui reçoit un callback
function saluer(nom, callback) {
  const message = `Bonjour ${nom}`;
  callback(message);
}

saluer("Yasmine", (msg) => console.log(msg));
console.log("Après saluer()");

Résultat dans la console

[ 2, 4, 6 ]
Bonjour Yasmine
Après saluer()

2. Les callbacks asynchrones

Un callback est asynchrone quand il est appelé plus tard, une fois une opération terminée. C'est le cas de setTimeout, addEventListener, et de nos propres fonctions qui simulent un serveur :

// Callback ASYNCHRONE : appelé plus tard, quand la donnée est prête
function chargerUtilisateur(id, callback) {
  setTimeout(() => {
    callback({ id: id, nom: "Amina" });
  }, 500);
}

chargerUtilisateur(1, (utilisateur) => {
  console.log("Utilisateur reçu :", utilisateur.nom);
});

console.log("Demande envoyée…");

Résultat dans la console

Demande envoyée…
Utilisateur reçu : Amina
⚠️ Attention
Une fonction asynchrone ne peut pas faire return utilisateur: au moment où elle se termine, la donnée n'existe pas encore. Elle doit la transmettre au callback quand elle est prête.

3. Gérer les erreurs : la convention « error-first »

Une opération asynchrone peut échouer (serveur injoignable, donnée invalide…). Un try/catch autour de l'appel ne sert à rien : l'erreur se produit plus tard, dans un autre tour de boucle. La convention, très utilisée par Node.js, est de passer l'erreur en premier argument du callback : null si tout va bien, un objet Error sinon.

// Convention « error-first » : callback(erreur, résultat)
function diviser(a, b, callback) {
  setTimeout(() => {
    if (b === 0) {
      callback(new Error("Division par zéro"));
      return;
    }
    callback(null, a / b);
  }, 300);
}

function afficher(err, resultat) {
  if (err) {
    console.log("Erreur :", err.message);
    return;
  }
  console.log("Résultat :", resultat);
}

diviser(10, 2, afficher);
diviser(10, 0, afficher);

Résultat dans la console

Résultat : 5
Erreur : Division par zéro

4. Le « callback hell »

Problème : quand plusieurs opérations dépendent les unes des autres, il faut appeler la suivante à l'intérieur du callback de la précédente. Pour afficher le total de la première commande d'un utilisateur, il faut charger l'utilisateur, puis ses commandes, puis les détails, puis calculer le total :

// Fonctions simulées (chacune « interroge un serveur » pendant 300 ms)
function chargerUtilisateur(id, cb) {
  setTimeout(() => cb(null, { id, nom: "Amina" }), 300);
}
function chargerCommandes(utilisateur, cb) {
  setTimeout(() => cb(null, [{ ref: "C-101" }, { ref: "C-102" }]), 300);
}
function chargerDetails(commande, cb) {
  setTimeout(() => cb(null, { ref: commande.ref, articles: [12.5, 7, 30] }), 300);
}
function calculerTotal(details, cb) {
  setTimeout(() => cb(null, details.articles.reduce((s, p) => s + p, 0)), 300);
}

// Le « callback hell »
chargerUtilisateur(1, (err, utilisateur) => {
  if (err) return console.log("Erreur :", err.message);
  chargerCommandes(utilisateur, (err, commandes) => {
    if (err) return console.log("Erreur :", err.message);
    chargerDetails(commandes[0], (err, details) => {
      if (err) return console.log("Erreur :", err.message);
      calculerTotal(details, (err, total) => {
        if (err) return console.log("Erreur :", err.message);
        console.log(`${utilisateur.nom} — commande ${details.ref} : ${total} DT`);
      });
    });
  });
});

Résultat dans la console

Amina — commande C-101 : 49.5 DT

Pyramide du callback hell et ses problèmes
Figure 3 — La « pyramide de l'enfer » et ses quatre inconvénients.

Ce code fonctionne, mais :

  • il « dérive » vers la droite : quatre étapes, quatre niveaux d'indentation ;
  • la gestion d'erreur est recopiée à chaque niveau, et un oubli fait disparaître l'erreur ;
  • exécuter deux étapes en parallèle, ou ajouter une étape, devient compliqué ;
  • inversion de contrôle: on confie la suite de son programme à une fonction qu'on ne maîtrise pas. Qui garantit qu'elle appellera le callback une seule fois, et pas zéro ou deux fois ?

5. Un premier remède : nommer les fonctions

Sortir chaque callback dans une fonction nommée supprime la pyramide :

function chargerUtilisateur(id, cb) {
  setTimeout(() => cb(null, { id, nom: "Amina" }), 300);
}
function chargerCommandes(utilisateur, cb) {
  setTimeout(() => cb(null, [{ ref: "C-101" }, { ref: "C-102" }]), 300);
}
function chargerDetails(commande, cb) {
  setTimeout(() => cb(null, { ref: commande.ref, articles: [12.5, 7, 30] }), 300);
}
function calculerTotal(details, cb) {
  setTimeout(() => cb(null, details.articles.reduce((s, p) => s + p, 0)), 300);
}

// Même traitement, avec des fonctions nommées : plus de pyramide
function gererErreur(err) {
  console.log("Erreur :", err.message);
}

function quandUtilisateur(err, utilisateur) {
  if (err) return gererErreur(err);
  chargerCommandes(utilisateur, quandCommandes);
}
function quandCommandes(err, commandes) {
  if (err) return gererErreur(err);
  chargerDetails(commandes[0], quandDetails);
}
function quandDetails(err, details) {
  if (err) return gererErreur(err);
  calculerTotal(details, (err, total) => {
    if (err) return gererErreur(err);
    console.log(`Commande ${details.ref} : ${total} DT`);
  });
}

chargerUtilisateur(1, quandUtilisateur);

Résultat dans la console

Commande C-101 : 49.5 DT

C'est plus lisible, mais la logique est éparpillée, et les autres problèmes restent. La vraie solution, ce sont les promesses (leçon 4).

ℹ️ À retenir
• Un callback est une fonction passée en argument, appelée plus tard par une autre fonction.
• Une fonction asynchrone transmet son résultat au callback ; elle ne peut pas le renvoyer avec return.
• Convention error-first : callback(erreur, resultat), avec erreur === null en cas de succès.
• Des opérations dépendantes imbriquées donnent le « callback hell » : peu lisible, erreurs répétées, inversion de contrôle.

🛠 Activité 3 — Écrire, imbriquer puis démêler des callbacks

Mission : Créez une petite « API simulée » de bibliothèque avec des callbacks, puis observez l'apparition du callback hell.

Questions pour avancer (répondez dans votre compte rendu, puis appliquez dans votre projet) :

  1. Écrivez rechercherLivre(titre, callback) qui, après 400 ms, renvoie { titre, id: 7 }, ou une erreur si le titre est vide. Quelle convention utilisez-vous pour le callback ?
  2. Écrivez verifierDisponibilite(id, callback) (300 ms, renvoie true ou false) et reserver(id, callback) (500 ms). Enchaînez les trois pour réserver « Le Petit Prince ». Combien de niveaux d'indentation obtenez-vous ?
  3. Que se passe-t-il si vous oubliez le return dans if (err) return …? Provoquez l'erreur et décrivez le comportement.
  4. Entourez l'appel à rechercherLivre("") d'un try { … } catch et faites lever une exception (throw) dans setTimeout. Le catch l'attrape-t-il ? Expliquez avec la boucle d'événements.
  5. Réécrivez l'enchaînement avec des fonctions nommées. Qu'avez-vous gagné ? Que reste-t-il de difficile ?
  6. Imaginez que reserver, écrite par un autre développeur, appelle par erreur votre callback deux fois. Quelle conséquence pour l'utilisateur ? Quel nom porte ce problème dans la leçon ?

Je vérifie / je rends :

  • ☐ Le fichier bibliotheque-callbacks.js fonctionnel.
  • ☐ Les réponses aux questions 3 à 6.

← 18.2 — La boucle d'événements (event loop)18.4 — Les promesses (Promise, then, catch, Promise.all) →