Avant les promesses, il fallait bien un moyen de dire au langage : voici ce qu'il faudra faire quand ce sera prêt. La réponse tient en une idée simple, et aussi vieille que JavaScript.
On passe la suite du travail sous forme de fonction, et c'est le code appelé qui décide du moment où il la déclenche.
Définition
Un callback, ou fonction de rappel, est une function passée en argument à une autre fonction, laquelle se charge de l'appeler. Elle n'est pas exécutée à l'endroit où on l'écrit, elle est confiée.
const utilisateurs = ["Chloé", "Marc", "Inès"];
// Appelé tout de suite, une fois par élément
utilisateurs.forEach((nom) => console.log(nom));
// Mis de côté, et appelé une seconde plus tard
setTimeout(() => console.log("Une seconde s'est écoulée"), 1000);Les deux écritures portent le même nom et ne font pas du tout la même chose. Array.forEach() rappelle immédiatement, setTimeout() rappelle plus tard. La différence ne se voit pas dans la syntaxe, seulement dans la documentation de la fonction appelée.
La convention erreur en premier
Une opération qui peut échouer doit pouvoir annoncer son échec. Node.js a imposé une règle devenue un standard : le callback reçoit l'erreur en premier argument, le résultat en second.
function lireConfig(chemin, callback) {
if (!chemin) return callback(new Error("Chemin manquant"));
callback(null, { theme: "sombre" });
}
lireConfig("app.json", (err, config) => {
if (err) return console.log("Échec :", err.message);
console.log(config.theme); // sombre
});Le null en première position signifie que rien n'a cassé. Un callback qui ne teste pas err avale les échecs en silence, et c'est de loin le défaut le plus fréquent de ce style d'écriture.
La pyramide, et pourquoi on en est sorti
Un callback dans un callback dans un callback : chaque étape décale la suivante vers la droite, et le test d'erreur se répète à chaque niveau.
lireConfig("app.json", (err, config) => {
if (err) return console.log(err.message);
lireProfil(config, (err, profil) => {
if (err) return console.log(err.message);
lireFactures(profil, (err, factures) => {
if (err) return console.log(err.message);
console.log(factures.length);
});
});
});Une Promise règle les deux problèmes ensemble : la suite d'étapes redevient plate avec then(), et un seul catch couvre toute la chaîne. Avec async et await, il ne reste que trois lignes qui se lisent de haut en bas.
Le callback n'a pas disparu pour autant. Les méthodes de tableau comme Array.map() ou Array.filter() en prennent un, addEventListener aussi, et tout ce qui se déclenche plusieurs fois continue d'en dépendre. Seules les opérations à résultat unique sont passées aux promesses.
Questions fréquentes
Un callback est-il forcément asynchrone ?
Non, et la confusion est tenace. Le callback de forEach ou de map s'exécute immédiatement, sur place, avant la ligne suivante. Celui de setTimeout attend son tour. Le mot ne dit rien du moment de l'appel : il faut regarder ce que fait la fonction qui le reçoit.
Pourquoi mon callback perd-il la valeur de this ?
Parce qu'une fonction classique reçoit son this de la façon dont elle est appelée, et le code qui la rappelle l'appelle sans contexte. Une fonction fléchée règle le problème : elle n'a pas de this propre et garde celui de l'endroit où elle a été écrite.
Faut-il encore apprendre ce style ?
Oui, car il est partout : événements du navigateur, méthodes de tableau, bibliothèques anciennes que l'on croise encore. Comprendre le callback rend d'ailleurs les promesses beaucoup plus claires, puisqu'elles ne font qu'organiser la même idée. Les deux styles sont travaillés côte à côte dans la formation JavaScript.