Il existe une croyance tenace chez beaucoup de développeurs JavaScript : callback rimerait forcément avec asynchrone. C’est faux, et cette confusion brouille la compréhension de mécanismes pourtant fondamentaux du langage. Un callback n’est ni synchrone ni asynchrone par nature : c’est le contexte dans lequel on l’invoque qui en décide. Démêler ce malentendu, c’est se donner les moyens de raisonner clairement sur l’Event Loop, sur l’ordre d’exécution du code, et sur un principe d’architecture aussi discret que central : l’Inversion of Control.
Les exemples ci-dessous sont écrits en JavaScript, mais les concepts abordés sont universels : on les retrouve, sous une forme ou une autre, dans la plupart des langages.
Qu’est-ce qu’un callback, au juste ?
Un callback est simplement une fonction transmise en argument à une autre fonction. Sa vocation est d’être exécutée ultérieurement, à un moment contrôlé et déterminé par la fonction qui le reçoit. Rien de plus.
function runMain(callback) {
// exécution des tâches [...]
// une fois le traitement fini, on invoque le callback
callback("finished");
}
runMain((result) => {
console.log(`program ended: ${result}`);
});
À ce stade, il n’y a aucun débat synchrone contre asynchrone. On illustre uniquement le fonctionnement d’un callback : callback sera invoqué quand runMain l’aura décidé. La fonction appelée garde la main sur le moment de l’invocation, et c’est précisément ce qui fait l’intérêt du mécanisme.
Synchrone OU asynchrone, jamais par défaut
Un callback peut être synchrone ou asynchrone, et tout dépend du contexte dans lequel il est invoqué. Le « ou » est ici déterminant : un callback bien conçu est l’un ou l’autre, mais pas les deux à la fois. Cette nuance mérite à elle seule un développement complet, que j’aborde dans le second volet de cette série, Callbacks : pourquoi un callback ne doit jamais être synchrone et asynchrone.
Le callback synchrone
Un callback synchrone est invoqué avant que la fonction qui l’utilise ne termine son exécution. L’appel du callback et celui de la fonction se déroulent sur la même call stack : tout se passe en un seul flux d’exécution continu, sans rupture.
/**
* 1. Callbacks synchrones
*
* Un callback synchrone est un callback qui est invoqué
* avant que la fonction qui l'utilise termine son exécution.
* Ici, le callback est invoqué de manière synchrone sur
* l'ensemble des éléments du tableau.
*/
console.log("Avant");
[1, 2].forEach((n) => {
console.log(`Callback invoqué pour: ${n}`);
// [...]
});
console.log("Après");
// Affiche :
// "Avant"
// "Callback invoqué pour 1"
// "Callback invoqué pour 2"
// "Après"
Le callback passé à forEach est invoqué immédiatement, pour chaque élément du tableau, avant que forEach ne rende la main. L’ordre d’affichage le confirme : tout se déroule linéairement, dans l’ordre du code.
Le callback asynchrone
Un callback asynchrone, à l’inverse, est invoqué une fois que la fonction qui l’utilise a terminé son exécution. Il est donc découplé du flux principal et exécuté plus tard. Ce différé peut prendre plusieurs formes :
- une exécution au sein d’un autre thread ;
- une prise en charge par une Event Loop, qui invoquera le callback une fois l’opération asynchrone achevée.
Les callbacks asynchrones servent le plus souvent à gérer des opérations I/O (lecture de fichier, requête réseau, accès base de données) sans bloquer le programme principal pendant l’attente.
/**
* 2. Callbacks asynchrones
*
* Un callback asynchrone est un callback qui est
* indépendamment invoqué, une fois que la fonction
* qui l'utilise a terminé son exécution.
*/
console.log("Avant");
setTimeout(() => {
console.log("Callback invoqué");
}, 0);
console.log("Après");
// Affiche :
// "Avant"
// "Après"
// "Callback invoqué"
C’est ici que l’Event Loop entre en scène. Même avec un délai de 0, le callback de setTimeout n’est pas exécuté tout de suite : il est placé dans la Timer Queue de l’Event Loop et n’y sera repris qu’une fois la pile d’exécution courante vidée. D’où l’ordre d’affichage : "Avant", puis "Après", et seulement ensuite "Callback invoqué". Le setTimeout ne « met pas en pause » le programme ; il programme une exécution future.
const timeout = setTimeout(() => {
console.log("setTimeout callback");
}, 0);
console.log("after setTimeout function");
// Affiche :
// 1. "after setTimeout function"
// 2. "setTimeout callback"
Le même phénomène se vérifie ici : la ligne synchrone s’exécute d’abord, le callback de la Web API setTimeout ensuite, parce qu’il transite par la file d’attente de l’Event Loop.
Au-delà de l’asynchrone : l’Inversion of Control
Si un callback n’a pas besoin d’être asynchrone, alors quel est l’intérêt d’en utiliser un dans un contexte purement synchrone ? La réponse tient en trois lettres : IoC, pour Inversion of Control.
Un callback est l’un des moyens les plus simples d’implémenter ce principe, magnifiquement résumé par le « Hollywood Principle » : Don’t call us, we’ll call you back. Vous ne contrôlez plus le moment de l’invocation ; vous le déléguez à la fonction qui reçoit votre callback. C’est elle qui décide quand, et dans quelles circonstances, votre code sera exécuté.
L’Inversion of Control est appliquée par d’innombrables runtimes, frameworks et bibliothèques, souvent sans que vous vous en rendiez compte. Vous implémentez votre code, vous respectez le contrat attendu, et l’acteur en charge du contrôle (la bibliothèque, le runtime, le framework) s’occupe de l’exécution au moment opportun. Le callback passé à forEach, le handler d’un événement, la route d’un serveur HTTP : autant d’exemples d’IoC qui ne dépendent en rien de l’asynchronisme.
Conclusion
Callback et asynchrone sont deux notions distinctes qu’il est temps de cesser de confondre. Un callback n’est qu’une fonction transmise pour être invoquée plus tard ; c’est le contexte d’invocation, même call stack ou file d’attente de l’Event Loop, qui en fait un callback synchrone ou asynchrone. Comprendre cette distinction, c’est non seulement raisonner juste sur l’ordre d’exécution de son code, mais aussi reconnaître dans le callback l’un des vecteurs les plus élégants de l’Inversion of Control.
Reste une question essentielle : pourquoi un callback ne doit-il jamais être à la fois synchrone et asynchrone ? C’est tout l’objet du second volet de cette série.