This looks like TypeScript with different punctuation. In what order do the three lines print?
async fn greet(name: &str) -> String {
println!("greet({name}) is running");
format!("hello, {name}")
}
#[tokio::main]
async fn main() {
let fut = greet("ada");
println!("future created");
let msg = fut.await;
println!("{msg}");
}In order: future created, then greet(ada) is running, then hello, ada.
If you guessed greet(ada) is running first, that is the TypeScript habit talking. In TS, calling an async function runs its body straight away, up to the first await. In Rust, greet("ada") ran none of the body. It only built a value, fut. The body ran when fut.await asked it to.
Here is what each piece did.
async fn greet(...) -> Stringdoes not return aString. The compiler turns the body into an anonymous type that implements theFuturetrait, and the function hands that value back 6. Its type isimpl Future<Output = String>. Think of it as a recipe for producing aString, not a promise that one is already on the way.let fut = greet("ada");builds the recipe. Nothing runs, because futures in Rust are lazy: they do nothing until you ask with.await1.fut.awaitruns the future until it finishes and gives you theOutput, here theString. If the future has to wait (on a timer, a socket),.awaitis where your function pauses and the runtime can do other work.
Two syntax notes for a TypeScript hand. .await is postfix: you write greet("ada").await, not await greet("ada"), which chains nicely as fetch().await.text().await. And .await takes ownership of the future, so you cannot await the same future twice the way you can re-await a resolved Promise.
One call is awaited, one is not. What does this print, and what does the compiler say?
async fn log_visit(user: u32) {
println!("visit from {user}");
}
#[tokio::main]
async fn main() {
log_visit(1);
log_visit(2).await;
println!("done");
}It prints visit from 2, then done.
visit from 1 never appears. log_visit(1) built a future and then dropped it at the semicolon, so its body never ran 2. The compiler tells you so, pointing at that line:
- warning: unused implementer of
Futurethat must be used - note: futures do nothing unless you
.awaitor poll them
It is a warning, not an error, so the program still builds. Treat this warning as a bug report every time you see it.
An async block is the same idea without a named function. async { ... } is an expression whose value is a future 6. The code inside it does not run where you write it; it runs when the block's future is awaited. Inside the block you can use .await, and the value of the last expression becomes the future's Output.
Async blocks are handy when you want a small piece of async work right where you are, for example to group a few awaits into one future you can pass around. You will meet async move { ... } soon too: it works the same but moves the variables it uses into the block, like a move closure.
#[tokio::main]
async fn main() {
let name = String::from("ada");
let count = async {
println!("block is running");
name.len()
};
println!("block created");
let n = count.await;
println!("{name} has {n} letters");
}block created, then block is running, then ada has 3 letters.So who runs a future? Try deleting #[tokio::main] from any example above and compiling. The compiler rejects it with error E0752: main function is not allowed to be async.
The reason main cannot be async is that async code needs a runtime, a crate that manages the details of executing asynchronous code; main can start one but is not one itself 5. Someone has to keep polling the future main would return, and there is nothing above main to do it.
This is a real difference from Node. Most languages that support async bundle a runtime, but Rust does not 8. The standard library defines the Future trait so that libraries agree on the shape of a future, but it has no executor to run one. Rust lets you pick a runtime to suit your needs instead of shipping one 7. For a web service that choice is almost always Tokio, which you add to Cargo.toml like any other crate.
What #[tokio::main] does to your main
Step 1 of 4 · You write
An
async fn main, which on its own would be rejected with E0752.rust#[tokio::main] async fn main() { println!("hello"); }Step 2 of 4 · Macro runs
#[tokio::main]is a macro. At compile time it turns yourasync fn maininto a normal, synchronousfn mainthat sets up a runtime and runs your async body on it 4.Step 3 of 4 · Expanded code
Roughly what the compiler sees. Your old body becomes an async block, and
block_ondrives that future to completion on the current thread, returning when it finishes. The real expansion usestokio::runtime::Builderto configure the runtime, but the shape is the same.rustfn main() { let rt = tokio::runtime::Runtime::new().unwrap(); rt.block_on(async { println!("hello"); }) }Step 4 of 4 · Inside the body
Every
.awaitinside the body now has a runtime underneath it that can pause and resume your code. That is why all the earlier examples worked once#[tokio::main]was onmain.
A tiny request handler with two async fns and an async block
Toward your HTTP service: write handle(id) that loads a user from a pretend database (100 ms) and returns a response string. Handle requests 1 and 2 from main, time them, then build request 3 as an async block and await it later. Needs tokio with the full feature.
Write the slow dependency.
tokio::time::sleepreturns a future; awaiting it pauses this function for 100 ms without blocking the thread. Note the.await: without it, the sleep would be built and dropped, and the function would return instantly.rustuse std::time::{Duration, Instant}; use tokio::time::sleep; async fn load_user(id: u32) -> String { sleep(Duration::from_millis(100)).await; // pretend database call format!("user-{id}") }Write the handler. Calling
load_user(id)builds a future;.awaitruns it and hands back theString. An async fn can await other async fns, just like in TypeScript.rustasync fn handle(id: u32) -> String { let user = load_user(id).await; format!("200 OK: {user}") }Call it from an async
mainunder#[tokio::main]. Each.awaitfinishes before the next line starts, so two requests take about 200 ms. Awaiting in sequence is not concurrency; running them at the same time comes in a later lesson.rust#[tokio::main] async fn main() { let start = Instant::now(); let first = handle(1).await; let second = handle(2).await; println!("{first} | {second} | took {:?}", start.elapsed());Build request 3 as an async block. The
println!after it prints first, proving the block has not started. Awaitingjobrunshandle(3)and returns the block's last expression, the body length.rustlet request_id = 3; let job = async { let body = handle(request_id).await; body.len() }; println!("job built, nothing ran yet"); let len = job.await; println!("response length: {len}"); }Run it. The output, with timing varying slightly:
200 OK: user-1 | 200 OK: user-2 | took 204.246375ms(the exact digits change every run)job built, nothing ran yetresponse length: 14
Async fns and async blocks both produce futures. Nothing inside them runs until something awaits them, and #[tokio::main] supplies the runtime that makes the top-level awaits possible.
Inside an async handler you write send_welcome_email(&user); with no .await. What happens at runtime?
Futures are lazy: without
.await(or a spawn) nobody runs the body 2. Theunused implementer of Futurewarning is pointing at exactly this bug.That is how an un-awaited JavaScript Promise or C# Task behaves. A Rust future is not running work; it is a value that runs only when awaited or handed to an executor 3.
That is the TypeScript rule for the synchronous part of an async function. In Rust, not even the first line of the body runs until the future is polled.
It compiles. The compiler only warns, which is why this bug can slip through if you ignore warnings.
A fresh cargo new project with no dependencies has async fn main() { println!("hi"); }. What happens?
Async code needs a runtime and
mainis not one 5.stddefinesFuturebut does not include an executor to run it.There is no built-in executor. Unlike Node or C#, Rust does not bundle an async runtime 8; you add one such as Tokio.
Good instinct about laziness, but the compiler stops earlier: an async
mainis rejected outright.
What does #[tokio::main] turn async fn main() { body } into?
The macro rewrites main into a sync function that starts a runtime and runs your async body on it 4.
There is no runtime hidden inside
stdto switch on. The runtime comes from thetokiocrate you added toCargo.toml.Awaits do not create threads. They are points where the future can pause so the runtime can run something else.
Given let b = async { println!("x"); 1 };, what is true right after this line?
An async block compiles to an anonymous type that implements Future 6. Its body waits for an await like any other future.
A plain
{ ... }block runs immediately and gives its last value. Addingasyncchanges it into a future instead.This mixes the two models. None of the block runs until it is awaited, including the println.
Write async fn check_stock(item: &str) -> u32 that prints checking {item}, sleeps 50 ms with tokio::time::sleep, and returns the item name's length as a u32 (a stand-in for a stock count).
In a #[tokio::main] main:
- call
check_stock("lamp")and store the result in_forgottenwithout awaiting it - build an async block that awaits
check_stock("desk")andcheck_stock("chair")and returns their sum - print
block built, then await the block and printtotal stock: {n}
Before running it, write down exactly which lines you expect to print, in order. Then run it and compare.
- Hint 1Start from
load_userin the worked example: an async fn that awaitssleep(Duration::from_millis(50))before returning. - Hint 2The async block looks like
let total = async { let a = ...await; let b = ...await; a + b }; - Hint 3Binding to
_forgottensilences the unused-future warning, but it does not run the future. Willchecking lampever print?
The output, in order:
block builtchecking deskchecking chairtotal stock: 9
checking lamp never prints: that future is built, stored and dropped at the end of main without being awaited. block built prints before any checking line because the async block does not start until total.await. Inside the block the two awaits run one after the other, desk then chair, and 4 + 5 = 9.
use std::time::Duration;
use tokio::time::sleep;
async fn check_stock(item: &str) -> u32 {
println!("checking {item}");
sleep(Duration::from_millis(50)).await;
item.len() as u32
}
#[tokio::main]
async fn main() {
let _forgotten = check_stock("lamp");
let total = async {
let a = check_stock("desk").await;
let b = check_stock("chair").await;
a + b
};
println!("block built");
let n = total.await;
println!("total stock: {n}");
}