On les confond presque toujours, et c’est compréhensible : evaluation strategy et execution strategy parlent toutes deux du moment où « les choses se passent » dans un programme. Pourtant ce sont deux axes parfaitement orthogonaux. L’un répond à la question quand une expression produit-elle sa valeur ?, l’autre à comment le programme déroule-t-il ses instructions ?. Les amalgamer mène à des intuitions fausses : typiquement croire qu’« asynchrone » signifie « différé », ou qu’une Promise ne fait rien tant qu’on ne l’attend pas. Démêlons les deux.
Deux dimensions à ne pas confondre
Tout repose sur une distinction de vocabulaire :
- La nature d’évaluation d’une expression : elle peut être eager (immédiate) ou lazy (différée).
- La nature d’exécution d’une instruction : elle peut être synchrone (bloquante) ou asynchrone (non bloquante).
La première décrit à quel instant une expression est transformée en valeur. La seconde décrit de quelle manière le moteur exécute le travail une fois lancé. Ce sont deux décisions indépendantes, et c’est précisément ce qui rend les quatre combinaisons possibles.
Evaluation Strategy : eager ou lazy
La stratégie d’évaluation concerne le moment et la manière dont une expression est utilisée pour produire une valeur. C’est le terrain des notions de lazy evaluation (évaluation différée) et eager evaluation (évaluation immédiate).
Eager : l’évaluation immédiate
En mode eager, l’expression est évaluée dès qu’elle est rencontrée. Ce mode autorise un style direct : on obtient une valeur, ou l’on déclenche un effet de bord, sans cérémonie. La contrepartie est qu’on perd la main sur le moment où ce travail démarre : il commence tout seul.
L’exemple canonique en JavaScript est la Promise : elle est eagerly evaluated. Dès qu’une Promise est créée, son exécuteur part immédiatement en phase d’exécution, sans attendre le moindre await ni .then().
// Le corps de la Promise s'exécute DÈS la création,
// avant même qu'on s'abonne au résultat.
const promise = new Promise((resolve) => {
console.log("évaluation immédiate (eager)");
resolve(42);
});
// "évaluation immédiate (eager)" est déjà affiché ici,
// indépendamment de ce qui suit.
promise.then((value) => console.log(value));
Lazy : l’évaluation différée
En mode lazy, l’évaluation de l’expression est repoussée pour n’avoir lieu qu’au moment où l’on en a réellement besoin. L’avantage est le contrôle total sur le déclenchement : rien ne se produit tant qu’on ne le demande pas explicitement.
L’exemple canonique en JavaScript est le Generator : il est lazily evaluated. Créer un générateur ne produit aucune valeur ; le corps ne progresse que lorsque l’itérateur sous-jacent est sollicité via next().
function* numbers() {
console.log("évaluation différée (lazy)");
yield 1;
yield 2;
}
// Aucune valeur produite, le corps n'a pas encore tourné.
const iterator = numbers();
// C'est SEULEMENT ici que "évaluation différée (lazy)"
// s'affiche et que la première valeur est produite.
iterator.next(); // { value: 1, done: false }
Execution Strategy : synchrone ou asynchrone
Une fois l’expression évaluée, qu’elle l’ait été immédiatement en eager ou de façon différée en lazy, l’exécution proprement dite peut prendre deux natures.
Synchrone : bloquant
L’exécution synchrone est dite bloquante : le contrôle n’est pas rendu au programme tant que l’opération n’est pas terminée. Autrement dit, le programme est entièrement mobilisé par les étapes synchrones, exécutées les unes après les autres, dans l’ordre.
// Chaque ligne attend la précédente : le contrôle ne revient
// qu'une fois l'instruction terminée.
const a = compute(1);
const b = compute(2);
console.log(a + b);
Asynchrone : non bloquant
L’exécution asynchrone est dite non bloquante : l’opération est lancée puis poursuivie en arrière-plan, par exemple via l’Event Loop, et le contrôle est immédiatement rendu au programme. Le résultat sera notifié dès qu’il est disponible, et pendant ce temps d’autres opérations peuvent progresser.
// L'opération est déléguée ; le contrôle revient aussitôt
// et la suite du programme continue.
setTimeout(() => console.log("notifié plus tard"), 0);
console.log("exécuté d'abord");
// Affiche : "exécuté d'abord" puis "notifié plus tard".
Les deux axes se combinent
Le point essentiel est que ces deux dimensions sont indépendantes. La stratégie d’évaluation détermine quand une expression est évaluée ; la stratégie d’exécution définit comment le programme exécute ses instructions. Aucune des deux ne dicte l’autre.
C’est ce qui rend possible des combinaisons que l’intuition tend à exclure :
- Lazy et synchrone : un générateur dont on consomme les valeurs dans une boucle bloquante. L’évaluation est différée, mais chaque étape s’exécute de façon synchrone.
- Eager et asynchrone : une Promise, justement. Son corps part immédiatement (eager), mais le résultat est livré de façon non bloquante (async).
Une évaluation peut donc être lazy tout en étant exécutée de manière synchrone, ou être eager tout en s’exécutant de manière asynchrone. Quand et comment sont deux réglages distincts, libres de se combiner.
Conclusion
Eager/lazy et synchrone/asynchrone ne sont pas deux façons de nommer la même chose : ce sont deux axes orthogonaux. Le premier répond à quand une expression est évaluée, le second à comment elle est exécutée. Garder cette distinction en tête évite des contresens tenaces, comme attendre d’une Promise qu’elle se comporte paresseusement, ou supposer qu’asynchrone implique différé. Dès qu’on raisonne sur les deux dimensions séparément, le comportement des Promises, des générateurs et de l’Event Loop cesse d’être surprenant pour devenir parfaitement prévisible.