The ProblemYou're likely building a service—perhaps fetching a JSON payload with reqwest or querying a database with sqlx. You drop an .await on a function call, expecting it to work, but the compiler throws a wrench in your gears:
error[E0728]: 'await' is only allowed inside 'async' functions and blocks
--> src/main.rs:10:5
|
9 | fn main() {
| ---- this is not 'async'
10 | let response = my_async_function().await;
| ^^^^^^^^^^^^^^^^^^^^^^^^^ only allowed inside 'async' functions and blocks
Root CauseRust's async model is unique. Unlike Node.js or Python, where tasks might run in the background automatically, Rust futures are "lazy." They sit idle and do absolutely nothing until something actually polls them.
When you call .await, you are signaling the compiler to transform your function into a state machine that can pause and resume execution. This state-shifting magic only works inside a context designed for it. Standard synchronous functions don't know how to pause. If you try to use .await inside one, the compiler hits a dead end and throws E0728.
How to Fix the Error### 1. Mark the function as asyncThe quickest fix is usually the right one. Simply prepend async to your function definition. This tells Rust that the function doesn't return a value immediately; instead, it returns a Future that will eventually yield your result.
Before:
fn fetch_data() {
let data = some_async_call().await; // Error E0728
}
After:
async fn fetch_data() {
let data = some_async_call().await; // Compiles successfully
}
2. Initialize an Async Runtime for main()New developers often hit this wall in fn main(). By default, Rust's entry point is synchronous and cannot be awaited. To fix this, you need an executor to manage your tasks. Tokio is the industry standard, used by over 80% of async Rust projects.
First, add the 1.x version of tokio to your Cargo.toml:
[dependencies]
tokio = { version = "1", features = ["full"] }
Then, use the #[tokio::main] macro to transform your main function:
#[tokio::main]
async fn main() {
let result = my_async_task().await;
println!("Task completed: {:?}", result);
}
3. Bridging Async and Sync with block_onYou will eventually hit a wall where async isn't an option. Maybe you're implementing a synchronous trait like std::fmt::Display or integrating with a legacy codebase. In these "sync-only" zones, use block_on to force the current thread to wait for a future.
Using futures::executor::block_on:
use futures::executor::block_on;
fn sync_function() {
// This blocks the thread until the future resolves
let data = block_on(my_async_task());
println!("Got data: {:?}", data);
}
4. Using Async BlocksIf you only need a tiny slice of asynchronous logic within a larger synchronous scope, use an async block. Just remember that the block itself is a Future. It won't execute until you pass it to a runtime.
fn main() {
let my_future = async {
do_something().await;
};
// The code above hasn't run yet. We must execute it:
futures::executor::block_on(my_future);
}
VerificationRun cargo check to verify the fix. If E0728 vanishes, your syntax is solid. However, watch out for these runtime pitfalls:
- Runtime Nesting: Never call
block_oninside an existing async function. In runtimes like Tokio, this will cause an immediate panic.- Deadlocks: Ensure your async tasks actually have a way to finish, orblock_onwill hang your entire thread forever.## Prevention Tips- Start with Async: If your app touches the network or a disk, use#[tokio::main]from day one. "Async creep" is real—it's much harder to turn a sync codebase async later.- Propagate Async Upwards: Aim to keep your call stackasyncall the way up. Only useblock_onat the very top level or at strict boundaries where async is impossible.- Check Trait Support: Standard traits likeIteratordon't support async yet. If you need it, use theasync-traitcrate orStreamfrom thefuturescrate.

