Asynchronous Rust

Async Error Handling

Async error handling uses the exact same Result<T, E> and ? operator idioms as synchronous Rust: an async fn that can fail returns Result<T, E> (so its desugared return type is impl Future<Output = Result<T, E>>), and chaining .await? first awaits the future to obtain the Result and then propagates any Err(e) via ?.

Chaining .await? Across Asynchronous Steps

use std::future::ready;
use std::pin::pin;
use std::task::{Context, Poll, Waker};

#[derive(Debug)]
enum OrderError {
    InvalidQuantity,
    OutOfStock { item_id: u32 },
}

async fn validate_quantity(qty: u32) -> Result<u32, OrderError> {
    ready(if qty > 0 {
        Ok(qty)
    } else {
        Err(OrderError::InvalidQuantity)
    })
    .await
}

async fn reserve_inventory(item_id: u32, qty: u32) -> Result<String, OrderError> {
    ready(if qty <= 10 {
        Ok(format!("reserved {qty}x item #{item_id}"))
    } else {
        Err(OrderError::OutOfStock { item_id })
    })
    .await
}

async fn place_order(item_id: u32, qty: u32) -> Result<String, OrderError> {
    // `.await` resolves the Future to `Result<...>`, and `?` unwraps `Ok` or returns `Err`:
    let valid_qty = validate_quantity(qty).await?;
    let confirmation = reserve_inventory(item_id, valid_qty).await?;
    Ok(confirmation)
}

fn eval_ready<F: std::future::Future>(fut: F) -> F::Output {
    let mut pinned = pin!(fut);
    let mut cx = Context::from_waker(Waker::noop());
    match pinned.as_mut().poll(&mut cx) {
        Poll::Ready(out) => out,
        Poll::Pending => panic!("unexpected Pending"),
    }
}

fn main() {
    println!("order 1: {:?}", eval_ready(place_order(101, 3)));
    println!("order 2: {:?}", eval_ready(place_order(101, 0)));
    println!("order 3: {:?}", eval_ready(place_order(101, 50)));
}
order 1: Ok("reserved 3x item #101")
order 2: Err(InvalidQuantity)
order 3: Err(OutOfStock { item_id: 101 })

Type Inference in async { ... } Blocks Using ?

When using the ? operator inside a standalone async { ... } block (rather than an async fn with an explicit return signature), the compiler knows the Ok(val) type from the final expression, but may not know which concrete error type E ? should convert into via From::from (E0282: type annotations needed).

Ending the async block with Ok::<_, MyError>(value) (using a turbofish on Ok) specifies the exact error type for the entire async block in one place:

use std::future::ready;
use std::pin::pin;
use std::task::{Context, Poll, Waker};

fn main() {
    let task = async {
        let raw: &str = ready(Ok::<&str, String>("42")).await?;
        let parsed: u32 = raw.parse().map_err(|e: std::num::ParseIntError| e.to_string())?;

        // Turbofish `Ok::<u32, String>(...)` anchors both `T` and `E` for the `async` block!
        Ok::<u32, String>(parsed * 2)
    };

    let mut pinned = pin!(task);
    let mut cx = Context::from_waker(Waker::noop());
    println!("task output: {:?}", pinned.as_mut().poll(&mut cx));
}
task output: Ready(Ok(84))

Choosing Box<dyn Error + Send + Sync> for Multi-Threaded Async Tasks

In synchronous single-threaded code, Box<dyn std::error::Error> is often used as a type-erased error. In multi-threaded async code where futures must implement Send (F: Future + Send), holding a plain Box<dyn Error> across an .await point (or returning it from a tokio::spawn task) fails to compile because Box<dyn Error> is !Send! Always use Box<dyn std::error::Error + Send + Sync + 'static> for type-erased errors in async Rust.