Can you write this from memory?
Declare an async function called fetch_data that returns a String.
Async Rust enables concurrent I/O without threads. async fn returns a Future. .await waits for it. An async runtime (like Tokio) drives execution.
The concepts are straightforward, but the syntax has quirks. When do you need .await? Why doesn't your async fn run until awaited? How do you handle errors in async code?
This page focuses on async/await fundamentals.
Most async Rust code uses Tokio as its runtime. Add it to your Cargo.toml before anything else:
[dependencies]
tokio = { version = "1", features = ["macros", "rt-multi-thread", "time"] }
macros enables #[tokio::main] and #[tokio::test]. rt-multi-thread gives you the multi-threaded scheduler. Add "full" instead if you want everything.
The most common first error. You wrote .await in a regular fn:
// WRONG: .await outside async context
// fn main() {
// let data = fetch_data().await; // error[E0728]
// }
// RIGHT: Use #[tokio::main] to make main async
#[tokio::main]
async fn main() {
let data = fetch_data().await;
println!("{data}");
}
async fn fetch_data() -> String {
"data".to_string()
}
If you cannot make the function async (e.g., inside a trait impl), build a runtime manually:
fn main() {
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let data = fetch_data().await;
println!("{data}");
});
}
tokio::spawn requires Send + 'static because the runtime can move tasks between threads. Three common causes:
// WRONG: Rc is not Send
// use std::rc::Rc;
// let shared = Rc::new("data".to_string());
// tokio::spawn(async move {
// println!("{shared}"); // error[E0277]
// });
// RIGHT: Use Arc instead
use std::sync::Arc;
let shared = Arc::new("data".to_string());
tokio::spawn(async move {
println!("{shared}");
});
Other causes: holding a &reference across .await (not 'static), or capturing a non-Send type like Cell or MutexGuard (from std) across a yield point. The concurrency guide explains Send and Sync traits in detail.
async fn fetch() -> String {
"data".to_string()
}
// WRONG: Calling async fn without .await
// let result = fetch(); // result is a Future, not a String!
// println!("{result}"); // ERROR: Future doesn't implement Display
// RIGHT: Await the future
#[tokio::main]
async fn main() {
let result = fetch().await; // now it's a String
println!("{result}");
}
async fn desugars to a regular function that returns an impl Future. The future does nothing until an executor polls it. .await is the common way to drive that polling.
This is different from JavaScript promises, which start executing immediately. In Rust, fetch() without .await just constructs the future — no network call happens, no work runs.
| Pattern | What it does | Requires Send? |
|---|---|---|
future.await | Sequential — one future at a time | No |
tokio::join!(a, b) | Concurrent within the current task | No |
tokio::spawn(fut) | New background task on the runtime | Yes (Send + 'static) |
tokio::spawn_blocking(f) | Blocking/CPU work on a dedicated thread pool | Yes |
tokio::select! | Races futures, cancels losers | No |
Key distinction: join! runs multiple futures concurrently within a single task. spawn creates a new task that can be scheduled across threads, which is why it requires Send.
// join! — concurrent, same task, no Send needed
let (a, b) = tokio::join!(fetch_users(), fetch_posts());
// spawn — background task, must be Send + 'static
let handle = tokio::spawn(async { process_data().await });
let result = handle.await.unwrap();
// select! — race futures, cancel the loser
tokio::select! {
val = fetch_primary() => println!("primary: {val}"),
_ = tokio::time::sleep(Duration::from_secs(5)) => println!("timeout"),
}
Calling blocking I/O inside an async task starves the executor — other tasks on that thread cannot progress:
// WRONG: Blocking I/O starves the async executor
// async fn read_config() -> String {
// std::fs::read_to_string("config.toml").unwrap() // blocks!
// }
// RIGHT: Use tokio::fs for async file I/O
async fn read_config() -> String {
tokio::fs::read_to_string("config.toml").await.unwrap()
}
// Or for CPU-heavy / non-async-compatible work:
let result = tokio::task::spawn_blocking(|| {
expensive_computation()
}).await.unwrap();
Rule of thumb: if a function doesn't return a Future and takes more than a few microseconds, wrap it in spawn_blocking. Error propagation with ? works identically in async functions -- see error handling for the full pattern.
A lock guard held across an .await point keeps the lock held while the task is suspended, blocking other tasks:
// WRONG: MutexGuard held across .await
// async fn update(mutex: &tokio::sync::Mutex<Vec<String>>) {
// let mut guard = mutex.lock().await;
// let data = fetch_data().await; // guard still held!
// guard.push(data);
// }
// RIGHT: Await first, then lock
async fn update(mutex: &tokio::sync::Mutex<Vec<String>>) {
let data = fetch_data().await; // await first
let mut guard = mutex.lock().await; // then lock
guard.push(data); // quick update, guard drops
}
If you must hold a lock across .await, use tokio::sync::Mutex (not std::sync::Mutex). But prefer restructuring to minimize lock duration. Move closures play a key role in async tasks -- the closures guide covers capture modes and the move keyword that tokio::spawn requires.
- The Rust Async Book — official guide to async/await
- Tokio Tutorial — hands-on Tokio introduction
- Tokio: Spawning — why
spawnrequiresSend + 'static - How Async Works: Polling and Wakers — the machinery under the hood
When to Use Rust Async: async/await, Futures & Tokio Basics
- Use async for I/O-bound operations (network, file system).
- Use threads for CPU-bound operations instead.
- Use `tokio::join!` to run multiple futures concurrently.
- Use `tokio::select!` to race futures and handle the first completion.
- Use `tokio::spawn` to run a future in the background.
Check Your Understanding: Rust Async: async/await, Futures & Tokio Basics
What happens when you call an async function without .await?
Nothing executes. Async functions return a Future, which is lazy. The code inside only runs when the Future is polled (typically via .await or by an async runtime).
What You'll Practice: Rust Async: async/await, Futures & Tokio Basics
Common Rust Async: async/await, Futures & Tokio Basics Pitfalls
- Your code compiles but nothing happens — you called an async function without `.await`, so you got a `Future` instead of a result. Add `.await` after the call.
- You see `error[E0728]: await is only allowed inside async functions` — you used `.await` in a sync `fn`. Either make the function `async fn` or wrap the block in `async { }` and spawn it on a runtime.
- The runtime hangs or becomes unresponsive — you called blocking I/O (like `std::fs::read`) inside an async task, starving the executor. Use `tokio::fs` or `spawn_blocking` for sync work.
- You get a deadlock with `Mutex` across `.await` — the lock guard lives across the yield point, blocking other tasks from acquiring it. Scope the guard so it drops before `.await`, or use `tokio::sync::Mutex`.
- You see `error[E0277]: future cannot be sent between threads safely` — the future holds a non-Send type like `Rc` or a borrow across `.await`. Switch to `Arc` or restructure to drop the reference before yielding.
Rust Async: async/await, Futures & Tokio Basics FAQ
Why do I need a runtime like Tokio?
Futures are lazy and need something to poll them. A runtime manages task scheduling, I/O polling, and timer handling. Without it, async functions never execute.
What is the difference between async and threads?
Threads run in parallel on multiple cores. Async tasks run concurrently on fewer threads by yielding during I/O. Use async for I/O-bound work (thousands of connections), threads for CPU-bound work.
Can I use async in a library without choosing a runtime?
Yes. Return `impl Future` and let the caller provide the runtime. Avoid `#[tokio::main]` in libraries. Only executables should choose the runtime.
How do I handle errors in async code?
Use `Result` and `?` just like sync code. Async functions can return `Result<T, E>`. The `?` operator works with `.await`: `client.get(url).await?`.
What does Pin mean in async Rust?
Pin prevents a value from moving in memory. Some futures are self-referential and would break if moved. Pin guarantees the future stays in place while being polled.
Why does tokio::spawn require Send?
Tokio can move tasks between worker threads when they yield at an .await point. Send guarantees the future and everything it captures can safely cross thread boundaries. If your future holds an Rc or a non-Send type, the compiler rejects it.
How do I run a non-Send future?
Use tokio::task::LocalSet and spawn_local(). A LocalSet keeps tasks pinned to the current thread, removing the Send requirement. This is useful when working with non-Send types like Rc or thread-local resources.
Rust Async: async/await, Futures & Tokio Basics Syntax Quick Reference
async fn fetch() -> String { "data".to_string() }let data = fetch().await;#[tokio::main]
async fn main() {
let _ = fetch().await;
}let fut = async { 42 };let resp = client.get(url).await?;let (a, b) = tokio::join!(f1, f2);tokio::spawn(async {
println!("background task");
});tokio::select! {
v = f1 => println!("f1: {v}"),
v = f2 => println!("f2: {v}"),
}tokio::time::sleep(Duration::from_secs(1)).await;Rust Async: async/await, Futures & Tokio Basics Sample Exercises
Fill in the keyword to make this function asynchronous.
asyncFill in the return type annotation (which becomes the Future's Output type). The literal 42 defaults to this type.
i32Fill in the keyword to create an async block that returns 42.
async+ 32 more exercises