Vol. 8 · № 01 · Un atelier RustA Rust workshop

Future & .await, ou le calcul qui ne se lance pas Future & .await, or the computation that doesn't start

Un Future décrit un calcul, il ne le lance pas : non-await, il est inerte. Casser l'analogie avec la promesse JS qui part dès sa création — un bug silencieux fréquent. A Future describes a computation, it doesn't start it: un-awaited, it is inert. Breaking the analogy with the JS promise that fires on creation — a frequent silent bug.

AudienceAudience
Dev maîtrisant possession et threads (Vol 1, Vol 7) — venant souvent d'async JS Dev fluent in ownership and threads (Vol 1, Vol 7) — often from JS async
Format
Self-paced
ChapitresChapters
5
Date
Juin 2026 Jun 2026
≈ 18 min ●●●● AsyncFutureawait
SommaireContents ·
№ 01CadrageFraming3 min

La concurrence sans threads : attendre sans bloquer, pas calculer en parallèle.Concurrency without threads: waiting without blocking, not computing in parallel.

Le Vol 7 parlait de parallélisme : faire tourner plusieurs calculs en même temps, sur plusieurs cœurs. L'async répond à un besoin différent, qu'on confond sans cesse avec celui-là : attendre sans bloquer. Pense à un serveur qui tient dix mille connexions, dont chacune dort la plupart du temps en attendant le réseau. Lancer dix mille threads serait un gâchis — chacun coûte sa pile, son changement de contexte. On veut une poignée de threads qui jonglent entre des milliers de tâches en sommeil, reprenant chacune juste quand sa donnée arrive. C'est l'async. En Rust, une async fn ne retourne pas sa valeur : elle retourne un Future, une valeur qui décrit le calcul à mener. Et .await est l'opérateur qui dit « mène ce calcul à terme, mais si tu dois attendre, rends la main pour qu'autre chose avance ».Vol 7 spoke of parallelism: running several computations at once, on several cores. Async answers a different need, constantly confused with that one: waiting without blocking. Picture a server holding ten thousand connections, each asleep most of the time waiting on the network. Spawning ten thousand threads would be waste — each costs its stack, its context switch. You want a handful of threads juggling thousands of sleeping tasks, resuming each just when its data arrives. That's async. In Rust, an async fn doesn't return its value: it returns a Future, a value describing the computation to carry out. And .await is the operator that says 'carry this computation to completion, but if you must wait, yield so something else can advance'.

Concurrence ≠ parallélisme — deux besoins, deux outilsConcurrency ≠ parallelism — two needs, two tools
ASYNC — 1 thread, mille attentesASYNC — 1 thread, a thousand waits
un seul thread jongle entre des tâches qui dormentone thread juggles tasks that sleep
T1
T2
T3
T4

■ actif · ▢ en attente (.await) → la main passe à une autre tâche■ active · ▢ waiting (.await) → control passes to another task

THREADS — N cœurs, N calculsTHREADS — N cores, N computations
chaque cœur mène un calcul de bout en bouteach core runs a computation end to end
C1
C2
C3
C4

■ calcul plein → vrai parallélisme, sur plusieurs cœurs■ full computation → true parallelism, across cores

async fn, .await, et le runtimeasync fn, .await, and the runtime
// async fn ne RETOURNE pas un T — il retourne un Future<Output = T>
async fn charger(url: &str) -> String {
    // … une requête réseau : on ATTEND la réponse sans bloquer le thread
    reponse
}

#[tokio::main]                          // le runtime : il fournit l'exécuteur qui fait tourner les Futures
async fn main() {
    let page = charger("/index").await; // .await : suspend CETTE tâche jusqu'à ce que le Future soit prêt
    println!("{}", page.len());         // … et pendant l'attente, l'exécuteur fait avancer d'autres tâches
}

Trois éléments à repérer. async fn transforme le type de retour : charger ne rend pas un String mais un Future<Output = String>. .await est le point où l'on consomme ce Future — en suspendant la tâche courante le temps de l'attente, sans bloquer le thread. Et #[tokio::main] installe le runtime : la bibliothèque qui fournit l'exécuteur, le moteur qui fait réellement tourner les Future. La std définit le trait Future, mais ne fournit aucun exécuteur — ce choix t'appartient (tokio, smol…). Sans runtime, un Future n'a personne pour le faire avancer.Three elements to spot. async fn transforms the return type: charger returns not a String but a Future<Output = String>. .await is where you consume that Future — suspending the current task for the wait, without blocking the thread. And #[tokio::main] installs the runtime: the library providing the executor, the engine that actually runs the Futures. The std defines the Future trait, but ships no executor — that choice is yours (tokio, smol…). Without a runtime, a Future has no one to drive it.

Le réflexe du numéroThe issue's reflex

« Est-ce que j'attends une I/O, ou est-ce que je veux du calcul en parallèle ? » C'est la question qui tranche entre async et threads, et la confondre coûte cher dans les deux sens. Des milliers d'attentes réseau → async, un runtime, une poignée de threads. Du calcul lourd à répartir sur les cœurs → threads (Vol 7), sans runtime async. L'async ne rend aucun calcul plus rapide : il permet seulement de ne pas gaspiller un thread à ne rien faire pendant qu'on attend.'Am I waiting on I/O, or do I want computation in parallel?' That's the question deciding between async and threads, and confusing it costs dearly both ways. Thousands of network waits → async, a runtime, a handful of threads. Heavy computation to spread across cores → threads (Vol 7), no async runtime. Async makes no computation faster: it only lets you not waste a thread doing nothing while you wait.

I/O-bound vs CPU-boundI/O-bound vs CPU-bound

Le vocabulaire qui structure tout le volume. Un programme est I/O-bound quand son temps se passe surtout à attendre quelque chose d'extérieur — réseau, disque, base de données : le processeur est oisif, c'est l'async qui brille. Il est CPU-bound quand son temps se passe à calculer — compression, rendu, simulation : le processeur est saturé, ce sont les threads et les cœurs qu'il faut. La même application peut avoir les deux profils selon l'endroit ; le réflexe est de toujours savoir, à un point donné du code, lequel domine.The vocabulary that structures the whole volume. A program is I/O-bound when its time is spent mostly waiting on something external — network, disk, database: the CPU is idle, async shines. It's CPU-bound when its time is spent computing — compression, rendering, simulation: the CPU is saturated, threads and cores are what you need. The same application can have both profiles depending on the place; the reflex is to always know, at a given point in the code, which dominates.

№ 02La surpriseThe surprise4 min

Appeler une async fn ne lance rien : le Future attend qu'on le réclame.Calling an async fn launches nothing: the Future waits to be claimed.

Voici la surprise qui désarçonne tout dev venant de JavaScript, et le piège le plus courant du débutant en async Rust. La syntaxe est identique — async, await, presque mot pour mot. Le modèle, lui, est inversé. En JS, une promesse est eager : fetch(url) déclenche la requête à l'instant où on l'écrit, qu'on l'attende ou non. En Rust, un Future est lazy : appeler charger(url) ne fait strictement rien — on obtient un objet inerte qui décrit le travail à faire, et ce travail ne commence qu'au .await (ou à un spawn). La conséquence pratique mord vite : un Future qu'on oublie d'attendre ne s'exécute jamais. Pas de crash, pas d'exception — juste un avertissement du compilateur, et une fonction qui n'a tout simplement pas tourné. C'est le bug silencieux par excellence.Here's the surprise that throws every dev coming from JavaScript, and the most common beginner trap in async Rust. The syntax is identical — async, await, almost word for word. The model, though, is inverted. In JS, a promise is eager: fetch(url) triggers the request the instant you write it, awaited or not. In Rust, a Future is lazy: calling charger(url) does strictly nothing — you get an inert object describing the work to do, and that work only begins at .await (or a spawn). The practical consequence bites fast: a Future you forget to await never runs. No crash, no exception — just a compiler warning, and a function that simply didn't run. It's the silent bug par excellence.

Le Future ne démarre qu'au .awaitThe Future only starts at .await
// Rust : le Future est INERTE jusqu'au .await (lazy)
charger("/a");                // ✕ rien ne se passe — AUCUNE requête envoyée
//   warning: unused implementer of `Future` that must be used
//   note: futures do nothing unless you `.await` or poll them

let fut = charger("/a");      // gardé dans une variable : toujours inerte
let reponse = fut.await;      // ← c'est SEULEMENT ici que le travail démarre

// oublier le .await ? la fonction ne s'exécute JAMAIS. pas d'erreur fatale,
// juste un avertissement — le bug silencieux le plus fréquent en async Rust.

« Futures do nothing unless you .await or poll them » — le compilateur le dit lui-même. Un Future est une valeur comme une autre : la créer ne la déclenche pas, exactement comme construire une struct n'exécute aucune logique. C'est cohérent avec tout le reste de Rust — rien d'implicite, rien qui parte dans ton dos — mais c'est l'inverse exact du réflexe JS. Le corollaire heureux : tu contrôles précisément quand le travail commence, et tu peux composer des Future avant d'en lancer aucun.'Futures do nothing unless you .await or poll them' — the compiler says so itself. A Future is a value like any other: creating it doesn't trigger it, exactly as building a struct runs no logic. It's consistent with all the rest of Rust — nothing implicit, nothing firing behind your back — but it's the exact inverse of the JS reflex. The happy corollary: you control precisely when the work starts, and you can compose Futures before launching any.

En JS la promesse part à la création ; en Rust le Future attend le .await. Oublier de l'attendre, c'est écrire du code qui ne s'exécute jamais.In JS the promise fires on creation; in Rust the Future waits for .await. Forgetting to await it is writing code that never runs.
№ 03La mécaniqueThe mechanics5 min

Un Future est une machine à états que l'exécuteur fait avancer par poll.A Future is a state machine the executor advances by poll.

Sous la syntaxe, le mécanisme est étonnamment simple. Future est un trait avec une seule méthode, poll, qui rend Poll : soit Ready(valeur) si le calcul est terminé, soit Pending s'il faut encore attendre. L'exécuteur du runtime appelle poll ; tant qu'il reçoit Pending, il met la tâche de côté et va poller d'autres tâches prêtes. Comment sait-il quand revenir ? La tâche, en rendant Pending, laisse un waker — un signal que la ressource (le socket, le timer) déclenchera dès qu'elle sera prête, replaçant la tâche dans la file. Et le async fn que tu écris ? Le compilateur le réécrit en une machine à états : chaque .await devient un point de suspension où l'état local est sauvegardé, pour pouvoir reprendre exactement là plus tard. Tu écris du code linéaire ; le compilateur en fait un automate.Beneath the syntax, the mechanism is surprisingly simple. Future is a trait with a single method, poll, returning Poll: either Ready(value) if the computation is done, or Pending if more waiting is needed. The runtime's executor calls poll; as long as it gets Pending, it sets the task aside and polls other ready tasks. How does it know when to come back? The task, returning Pending, leaves a waker — a signal the resource (the socket, the timer) will fire as soon as it's ready, putting the task back in the queue. And the async fn you write? The compiler rewrites it into a state machine: each .await becomes a suspension point where local state is saved, to resume exactly there later. You write linear code; the compiler makes it an automaton.

La boucle de poll — Pending rend la main, le waker rappelleThe poll loop — Pending yields, the waker calls back
exécuteurexecutor
poll()
poll()▸
Future
machine à étatsstate machine
▸
Pending
laisse un waker → l'exécuteur fait autre choseleaves a waker → executor does other work
▸
Ready(T)
la valeur est là → .await la rendthe value is here → .await returns it
↺ waker : « ressource prête » → l'exécuteur re-poll la tâche, et seulement alors↺ waker: 'resource ready' → executor re-polls the task, and only then
Le trait, en une méthodeThe trait, in one method
trait Future {
    type Output;
    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}
enum Poll<T> { Ready(T), Pending }    // soit la valeur est prête, soit « repasse plus tard »

// .await se traduit (en gros) par : « poll ce Future ; s'il rend Pending, RENDS LA MAIN
// à l'exécuteur en laissant un waker, et reprends quand la ressource sera prête ».

Tout l'async tient dans ce Poll à deux variantes. Ready(T) : c'est fini, voici la valeur. Pending : pas encore, mais j'ai laissé un waker ; ne me rappelle pas en boucle, je te ferai signe. C'est la différence entre l'async et le busy-wait naïf : l'exécuteur ne re-poll pas sans cesse une tâche bloquée — il l'oublie jusqu'à ce que le waker la réveille. Pendant ce temps, le thread est libre pour d'autres tâches. C'est là, et seulement là, que naît le gain : un thread qui ne dort jamais à ne rien faire.All of async fits in this two-variant Poll. Ready(T): done, here's the value. Pending: not yet, but I've left a waker; don't poll me in a loop, I'll signal you. That's the difference between async and naive busy-wait: the executor doesn't re-poll a blocked task endlessly — it forgets it until the waker wakes it. Meanwhile, the thread is free for other tasks. That, and only that, is where the gain is born: a thread that never sleeps doing nothing.

L'async fn que le compilateur réécrit en automateThe async fn the compiler rewrites into an automaton
async fn requete() -> String {
    let socket = connecter().await;       // ← point de suspension 1
    let octets  = socket.lire().await;    // ← point de suspension 2
    decoder(octets)
}
// le compilateur RÉÉCRIT cette fonction en une machine à états : un enum dont chaque
// variante est « où en est-on entre deux .await ». poll() fait avancer d'un état au
// suivant ; entre deux appels, l'état (le socket, les variables locales) est SAUVEGARDÉ.
Le runtime, pièce manquante de la std.The runtime, the std's missing piece.

La std définit le trait Future, le type Poll, le waker — mais aucun exécuteur. C'est un choix délibéré : l'exécuteur (boucle d'événements, ordonnanceur, mono ou multi-thread) est un sujet à part entière, et Rust vise des cibles trop diverses (du serveur à l'embarqué) pour en imposer un. On le prend donc en bibliothèque : tokio, de loin le plus répandu, ou smol pour le minimalisme (async-std, longtemps cité, est abandonné depuis 2025). #[tokio::main] n'est qu'un sucre qui démarre le runtime et y lance ton main async. Sans lui, tes Future n'ont littéralement personne pour appeler poll.The std defines the Future trait, the Poll type, the waker — but no executor. It's a deliberate choice: the executor (event loop, scheduler, single- or multi-threaded) is a subject of its own, and Rust targets too diverse a range (from server to embedded) to impose one. So you take it as a library: tokio, by far the most widespread, or smol for minimalism (async-std, long cited, has been discontinued since 2025). #[tokio::main] is just sugar that starts the runtime and launches your async main in it. Without it, your Futures have literally no one to call poll.

Tu écris du code linéaire ; le compilateur en fait un automate, et l'exécuteur le fait avancer un poll à la fois.You write linear code; the compiler makes it an automaton, and the executor advances it one poll at a time.
№ 04Le piègeThe trap4 min

Tirer un runtime async pour du calcul — et bloquer l'exécuteur qu'on vient d'installer.Pulling an async runtime for computation — and blocking the executor you just installed.

Une fois async/await en main, la tentation est de tout y passer — comme on a vu le réflexe Mutex partout au Vol 7. Mais l'async ne sert qu'à une chose : ne pas gaspiller un thread pendant qu'on attend. Il ne rend aucun calcul plus rapide. Pire : un calcul lourd, ou un appel bloquant classique (std::thread::sleep, une lecture de fichier synchrone, une requête SQL bloquante) placé dans une tâche async, monopolise le thread de l'exécuteur — car il n'y a pas de .await pour rendre la main. Toutes les autres tâches de ce thread gèlent, le temps que le calcul finisse. Le gain de l'async s'effondre exactement là où on croyait l'étendre. Le contre-exemple le plus pur : monter tout un runtime pour un programme CPU-bound qui n'avait besoin que de threads.Once async/await is in hand, the temptation is to route everything through it — as we saw the Mutex-everywhere reflex in Vol 7. But async serves only one thing: not wasting a thread while you wait. It makes no computation faster. Worse: a heavy computation, or a classic blocking call (std::thread::sleep, a synchronous file read, a blocking SQL query) placed in an async task, monopolizes the executor's thread — because there's no .await to yield. All the other tasks on that thread freeze until the computation finishes. Async's gain collapses exactly where you thought you'd extend it. The purest counter-example: spinning up a whole runtime for a CPU-bound program that needed only threads.

Le contre-exemple — le bloquant qui gèle l'exécuteurThe counter-example — the blocker that freezes the executor
#[tokio::main]
async fn main() {
    tokio::join!(
        servir_requetes(),               // veut juste attendre le réseau (I/O-bound)
        async {
            std::thread::sleep(CINQ_S);  // ✕ appel BLOQUANT dans une tâche async
            compresser(gros_fichier);    //   calcul LOURD, aucun .await → ne rend jamais la main
        },
    );
    // pendant ces secondes, servir_requetes() est GELÉ : le thread de l'exécuteur
    // est monopolisé. on a tiré tokio… pour du CPU, qui voulait juste des threads.
}
thread de l'exécuteur — monopolisé par le bloquantexecutor thread — monopolized by the blocker
tâche CPU/bloquanteCPU/blocking task
servir_requetesservir_requetes

↳ aucun .await → l'I/O ne progresse pas tant que le calcul n'a pas fini↳ no .await → I/O makes no progress until the computation finishes

Le bon réflexe — attendre en async, calculer ailleursThe right reflex — wait in async, compute elsewhere
#[tokio::main]
async fn main() {
    tokio::join!(
        servir_requetes(),
        async {
            tokio::time::sleep(CINQ_S).await;              // ✓ attente qui REND la main
            tokio::task::spawn_blocking(|| compresser(gros_fichier))
                .await.unwrap();                           // ✓ le CPU part sur un pool dédié
        },
    );
}
// et si le programme est PUREMENT CPU-bound ? alors pas d'async du tout : des threads (Vol 7).
thread de l'exécuteur — libre de jongler · CPU sur un pool à partexecutor thread — free to juggle · CPU on a separate pool
tâche CPU/bloquanteCPU/blocking task
servir_requetesservir_requetes

↳ l'attente rend la main, l'I/O s'intercale, le CPU (■ ink) tourne ailleurs↳ the wait yields, I/O interleaves, the CPU (■ ink) runs elsewhere

Deux corrections, deux principes. L'attente passe par la version async de l'outil — tokio::time::sleep().await au lieu de std::thread::sleep — pour que la tâche rende la main au lieu de geler le thread. Et le calcul lourd sort de l'exécuteur via spawn_blocking, qui le confie à un pool de threads dédié, laissant les tâches I/O respirer. Mais le vrai réflexe est en amont : si le programme est essentiellement du calcul, l'async n'a rien à y faire. Des threads (Vol 7), un scope, un Arc<Mutex> si besoin — et pas une ligne de runtime async.Two fixes, two principles. The wait goes through the async version of the tool — tokio::time::sleep().await instead of std::thread::sleep — so the task yields instead of freezing the thread. And the heavy computation leaves the executor via spawn_blocking, which hands it to a dedicated thread pool, letting the I/O tasks breathe. But the real reflex is upstream: if the program is essentially computation, async has no business there. Threads (Vol 7), a scope, an Arc<Mutex> if needed — and not a line of async runtime.

async et threads ne s'opposent pasasync and threads aren't opposed

Le verdict n'est pas « l'un ou l'autre, à choisir une fois pour toutes ». Les grosses applications utilisent les deux : un runtime async pour l'immense majorité I/O-bound (servir des requêtes, parler à des bases), et des threads — souvent via spawn_blocking ou un pool séparé — pour les îlots CPU-bound (compression, crypto, traitement d'image). Le réflexe n'est pas tribal ; il est local : à ce point précis du code, est-ce que j'attends, ou est-ce que je calcule ?The verdict isn't 'one or the other, chosen once and for all'. Large applications use both: an async runtime for the vast I/O-bound majority (serving requests, talking to databases), and threads — often via spawn_blocking or a separate pool — for the CPU-bound islands (compression, crypto, image processing). The reflex isn't tribal; it's local: at this precise point in the code, am I waiting, or am I computing?

La règle « ne bloque jamais l'exécuteur »The 'never block the executor' rule

Dans une tâche async, tout ce qui ne rend pas la main est suspect : std::thread::sleep, une I/O synchrone, une boucle de calcul sans await, un Mutex std tenu longtemps. Chacun gèle le thread de l'exécuteur et, avec lui, toutes les tâches qu'il portait. Les bons runtimes offrent les contreparties : sleep async, I/O async, spawn_blocking pour isoler le bloquant, parfois un Mutex async. La règle se résume en une phrase : si ça peut attendre longtemps sans await, ça n'a rien à faire directement dans une tâche async.In an async task, anything that doesn't yield is suspect: std::thread::sleep, a synchronous I/O, a compute loop with no await, a std Mutex held long. Each freezes the executor's thread and, with it, every task it carried. Good runtimes offer the counterparts: async sleep, async I/O, spawn_blocking to isolate the blocker, sometimes an async Mutex. The rule sums up in one sentence: if it can wait a long time with no await, it has no business directly inside an async task.

L'async sert à ne pas gaspiller un thread qui attend — jamais à calculer plus vite. CPU-bound ? Ce sont des threads qu'il te faut, pas un runtime.Async serves to not waste a thread that waits — never to compute faster. CPU-bound? It's threads you need, not a runtime.
№ 05Bilan & éditoWrap-up & editorial2 min

Le Future ne ment pas : il décrit un calcul, et attend qu'on le réclame.The Future doesn't lie: it describes a computation, and waits to be claimed.

Ce numéro ouvre le Vol 8 en cassant l'analogie qui piège tout dev venant de JS. L'async, c'est la concurrence sans threads : pour attendre sans bloquer, pas pour calculer en parallèle. Une async fn retourne un Future — une valeur inerte qui décrit le travail, et ne le lance qu'au .await. Sous le capot, le compilateur réécrit ta fonction en une machine à états que l'exécuteur du runtime fait avancer par poll : Pending rend la main et laisse un waker, Ready livre la valeur. La std fournit le trait, pas l'exécuteur — d'où tokio. Et le piège ferme la boucle : l'async ne sert qu'à ne pas gaspiller un thread qui attend ; y mettre du calcul ou du bloquant gèle l'exécuteur et trahit tout le bénéfice. CPU-bound ? Des threads, pas un runtime.This issue opens Vol 8 by breaking the analogy that traps every dev coming from JS. Async is concurrency without threads: to wait without blocking, not to compute in parallel. An async fn returns a Future — an inert value describing the work, launching it only at .await. Under the hood, the compiler rewrites your function into a state machine the runtime's executor advances by poll: Pending yields and leaves a waker, Ready delivers the value. The std provides the trait, not the executor — hence tokio. And the trap closes the loop: async serves only to not waste a thread that waits; putting computation or blocking in it freezes the executor and betrays the whole benefit. CPU-bound? Threads, not a runtime.

La notionThe notionCe qu'il faut en retenirWhat to remember
async fnNe retourne pas un T mais un Future<Output = T>.Returns not a T but a Future<Output = T>.
Future (lazy)Future (lazy)Inerte tant qu'on ne l'await ni ne le spawn : rien ne part à la création.Inert until awaited or spawned: nothing fires on creation.
.awaitDéclenche le calcul ; si Pending, rend la main à l'exécuteur.Triggers the computation; if Pending, yields to the executor.
poll · wakerL'exécuteur fait avancer la machine à états ; le waker la réveille.The executor advances the state machine; the waker wakes it.
le runtimethe runtimeAbsent de la std : tokio, smol… fournissent l'exécuteur.Absent from std: tokio, smol… provide the executor.
async ≠ parallèleasync ≠ parallelPour attendre, pas pour calculer. CPU-bound → threads (Vol 7).For waiting, not computing. CPU-bound → threads (Vol 7).
La syntaxe est celle de JS ; le modèle est l'inverse — lazy, explicite, sans rien qui parte dans ton dos.The syntax is JS's; the model is the inverse — lazy, explicit, with nothing firing behind your back.
Mot de l'éditeurFrom the editor

Un mot-clé qui ne reste jamais seul : async remonte toute la pile d'appels.A keyword that never stays put: async climbs the whole call stack.

On sait maintenant ce qu'est un Future : une valeur paresseuse, une machine à états, un calcul qui attend son poll. Mais on l'a regardée seule, comme si une async fn vivait isolée. Or async a une propriété qui déborde de la fonction où on l'écrit : pour await un Future, il faut soi-même être dans un contexte async. Donc une fonction qui appelle de l'async devient async ; et celle qui l'appelle aussi ; et ainsi de suite jusqu'au main. Le mot-clé ne reste pas où on l'a posé — il colore, de proche en proche, toute la chaîne d'appels.We now know what a Future is: a lazy value, a state machine, a computation awaiting its poll. But we looked at it alone, as if an async fn lived in isolation. Yet async has a property that overflows the function where you write it: to await a Future, you must yourself be in an async context. So a function that calls async becomes async; and its caller too; and so on up to main. The keyword doesn't stay where you put it — it colors, step by step, the entire call chain.

Vol 8 №2 : le problème des fonctions colorées. On verra pourquoi async « contamine » les signatures de proche en proche, pourquoi on ne peut pas appeler simplement une async fn depuis du code synchrone, et la friction bien réelle que ça crée — deux mondes, sync et async, qui se mélangent mal. On verra aussi la contrainte qui surgit dès qu'une tâche async traverse des threads : la donnée tenue à travers un .await doit être Send, et ce petit mot rouvre, en plein async, toute la mécanique du Vol 7. La couleur a un prix ; il vaut mieux le connaître avant de peindre.Vol 8 №2: the function coloring problem. We'll see why async 'contaminates' signatures step by step, why you can't simply call an async fn from synchronous code, and the very real friction it creates — two worlds, sync and async, that mix poorly. We'll also see the constraint that arises the moment an async task crosses threads: data held across a .await must be Send, and that little word reopens, mid-async, the whole machinery of Vol 7. Color has a price; better to know it before painting.