async fn Returns a Future
Goal of This Episode
Build a key mental model: calling an async fn doesn’t execute it — you merely get a Future that hasn’t started running.
Main Text
Calling an async fn Doesn’t Run It
This is the pit beginners fall into most often, so let’s prove it by experiment. Start with an ordinary async fn:
extern crate tokio;
async fn say_hello() {
println!("hello");
}
#[tokio::main]
async fn main() {
say_hello(); // note: this line does NOT print hello!
}
Intuitively you’d expect calling say_hello() to print hello, but in fact nothing happens. The println! in the function body never runs. Not only that, the compiler gives you a warning:
warning: unused implementer of `Future` that must be used
note: futures do nothing unless you `.await` or poll them
This warning (courtesy of #[must_use]) already spills the truth: calling say_hello() gives you a Future — a “job that hasn’t run yet.” You’ve only described the job; nobody executed it, so it got thrown away.
To actually run it, add .await:
extern crate tokio;
async fn say_hello() {
println!("hello");
}
#[tokio::main]
async fn main() {
say_hello().await; // this time hello gets printed
}
Making the Compiler Confirm It’s a Future
Still not convinced? We can force the compiler to tell the truth another way: deliberately annotate the wrong return type and see how it complains.
extern crate tokio;
async fn say_hello() {
println!("hello");
}
#[tokio::main]
async fn main() {
let x: () = say_hello(); // compile error
}
say_hello’s body returns nothing, so it “should” return (), and we deliberately write let x: () = .... But the compiler errors:
expected `()`, found future
It tells you plainly: the type of say_hello() is not () but a future. Confirmed — calling an async fn gets you a Future, not the result of the body’s execution.
Futures Are Lazy
The two sections above showed two things: calling say_hello() doesn’t execute the body immediately; and say_hello()’s return type isn’t () but a Future. Put together, they give this episode’s most important sentence:
Calling an
async fnjust gets you aFuture, and thatFutureis lazy.
“Lazy” should sound familiar. Recall Chapter 6’s iterators: when you write v.iter().map(...).filter(...), those methods haven’t processed a single element — they only describe “what to do later”; the real running starts the moment you .collect() or walk it with for.
Future and Iterator share the same design philosophy at heart: describe first, execute later. An Iterator describes “how a sequence of values gets computed” and does work only when you ask it for values; a Future describes “what an async job will do” and makes progress only when the runtime pushes it forward.
Next episode we switch angles and set .await beside the ? you’ve long known — you’ll find they’re the same kind of thing.
Recap
- Calling an
async fndoes not execute the body; you only get aFuture. - Without a
.awaitinasync fn main, the calledasyncfunction won’t run a single line, and you’ll get a#[must_use]warning. - Annotating the return value as
()makes the compiler reportexpected (), found future, proving it really is aFuture. Futures are lazy — like Chapter 6’sIterator, the design is “describe first, execute later.”