Async Await Patterns

Need to write asynchronous Rust code? This guide covers async functions, futures, runtimes like Tokio, concurrent async operations, and common async patterns.

Problem: Making Async HTTP Requests

Scenario

You need to make non-blocking HTTP requests.

Solution: Use Tokio and Reqwest

[dependencies]
tokio = { version = "1.0", features = ["full"] }
reqwest = "0.11"
use reqwest;

#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
    let response = reqwest::get("https://httpbin.org/get")
        .await?
        .text()
        .await?;

    println!("Response: {}", response);
    Ok(())
}

How it works:

  • #[tokio::main]: Sets up async runtime
  • async fn: Function returns a Future
  • .await: Suspends function until Future completes

Problem: Running Multiple Async Operations Concurrently

Scenario

You want to run several async operations at the same time.

Solution 1: Use join!

use tokio;

async fn task1() -> String {
    tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
    String::from("Task 1 complete")
}

async fn task2() -> String {
    tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
    String::from("Task 2 complete")
}

#[tokio::main]
async fn main() {
    let (result1, result2) = tokio::join!(task1(), task2());
    println!("{}", result1);
    println!("{}", result2);
    // Both complete in ~1 second (concurrent, not sequential)
}

Solution 2: Use try_join! for Fallible Operations

use tokio;

async fn fetch_user(id: u32) -> Result<String, Box<dyn std::error::Error>> {
    // Simulated async operation
    tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
    Ok(format!("User {}", id))
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let (user1, user2, user3) = tokio::try_join!(
        fetch_user(1),
        fetch_user(2),
        fetch_user(3),
    )?;

    println!("{}, {}, {}", user1, user2, user3);
    Ok(())
}

How it works: try_join! runs all futures concurrently, returns first error or all successes.


Problem: Spawning Async Tasks

Scenario

You want to run async code in the background.

Solution: Use tokio::spawn

use tokio;
use tokio::time::{sleep, Duration};

#[tokio::main]
async fn main() {
    let handle = tokio::spawn(async {
        sleep(Duration::from_secs(1)).await;
        println!("Background task complete");
        42
    });

    println!("Doing other work...");

    // Wait for spawned task
    let result = handle.await.unwrap();
    println!("Result: {}", result);
}

Multiple tasks:

#[tokio::main]
async fn main() {
    let mut handles = vec![];

    for i in 0..10 {
        let handle = tokio::spawn(async move {
            sleep(Duration::from_millis(100)).await;
            println!("Task {} complete", i);
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.await.unwrap();
    }
}

Problem: Sharing State Across Async Tasks

Scenario

Multiple async tasks need to access shared data.

Solution: Use Arc<Mutex> or Arc<RwLock>

use tokio;
use std::sync::{Arc, Mutex};

#[tokio::main]
async fn main() {
    let counter = Arc::new(Mutex::new(0));
    let mut handles = vec![];

    for _ in 0..10 {
        let counter_clone = Arc::clone(&counter);
        let handle = tokio::spawn(async move {
            let mut num = counter_clone.lock().unwrap();
            *num += 1;
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.await.unwrap();
    }

    println!("Result: {}", *counter.lock().unwrap());
}

Better: Use tokio::sync::Mutex for async-aware locking:

use tokio::sync::Mutex;
use std::sync::Arc;

#[tokio::main]
async fn main() {
    let counter = Arc::new(Mutex::new(0));
    let mut handles = vec![];

    for _ in 0..10 {
        let counter_clone = Arc::clone(&counter);
        let handle = tokio::spawn(async move {
            let mut num = counter_clone.lock().await;  // .await instead of unwrap
            *num += 1;
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.await.unwrap();
    }

    println!("Result: {}", *counter.lock().await);
}

Problem: Async Channels for Message Passing

Scenario

Async tasks need to communicate.

Solution: Use tokio::sync::mpsc

use tokio::sync::mpsc;

#[tokio::main]
async fn main() {
    let (tx, mut rx) = mpsc::channel(32);

    tokio::spawn(async move {
        for i in 0..10 {
            tx.send(i).await.unwrap();
        }
    });

    while let Some(msg) = rx.recv().await {
        println!("Received: {}", msg);
    }
}

Multiple producers:

use tokio::sync::mpsc;

#[tokio::main]
async fn main() {
    let (tx, mut rx) = mpsc::channel(100);

    for i in 0..3 {
        let tx_clone = tx.clone();
        tokio::spawn(async move {
            tx_clone.send(format!("Message from task {}", i)).await.unwrap();
        });
    }
    drop(tx);  // Close channel

    while let Some(msg) = rx.recv().await {
        println!("{}", msg);
    }
}

Problem: Timeout for Async Operations

Scenario

You want to limit how long an async operation can take.

Solution: Use tokio::time::timeout

use tokio::time::{timeout, Duration, sleep};

async fn slow_operation() -> String {
    sleep(Duration::from_secs(5)).await;
    String::from("Complete")
}

#[tokio::main]
async fn main() {
    match timeout(Duration::from_secs(2), slow_operation()).await {
        Ok(result) => println!("Result: {}", result),
        Err(_) => println!("Operation timed out"),
    }
}

Problem: Select Between Multiple Async Operations

Scenario

You want to wait for the first of several operations to complete.

Solution: Use tokio::select!

use tokio::time::{sleep, Duration};

#[tokio::main]
async fn main() {
    let mut interval = tokio::time::interval(Duration::from_secs(1));

    loop {
        tokio::select! {
            _ = interval.tick() => {
                println!("Tick");
            }
            _ = tokio::signal::ctrl_c() => {
                println!("Ctrl-C received, exiting");
                break;
            }
        }
    }
}

Racing futures:

async fn task1() -> &'static str {
    sleep(Duration::from_secs(1)).await;
    "Task 1"
}

async fn task2() -> &'static str {
    sleep(Duration::from_secs(2)).await;
    "Task 2"
}

#[tokio::main]
async fn main() {
    tokio::select! {
        result = task1() => println!("First to complete: {}", result),
        result = task2() => println!("First to complete: {}", result),
    }
}

Problem: Async File I/O

Scenario

You need to read/write files asynchronously.

Solution: Use tokio::fs

use tokio::fs;

#[tokio::main]
async fn main() -> std::io::Result<()> {
    // Read file
    let contents = fs::read_to_string("file.txt").await?;
    println!("Contents: {}", contents);

    // Write file
    fs::write("output.txt", "Hello, async!").await?;

    // Copy file
    fs::copy("source.txt", "destination.txt").await?;

    Ok(())
}

Reading lines:

use tokio::fs::File;
use tokio::io::{AsyncBufReadExt, BufReader};

#[tokio::main]
async fn main() -> std::io::Result<()> {
    let file = File::open("file.txt").await?;
    let reader = BufReader::new(file);

    let mut lines = reader.lines();
    while let Some(line) = lines.next_line().await? {
        println!("{}", line);
    }

    Ok(())
}

Problem: Async Stream Processing

Scenario

You need to process items as they arrive.

Solution: Use Streams

use tokio_stream::{self as stream, StreamExt};
use tokio::time::{sleep, Duration};

#[tokio::main]
async fn main() {
    let mut stream = stream::iter(vec![1, 2, 3, 4, 5])
        .then(|x| async move {
            sleep(Duration::from_millis(100)).await;
            x * 2
        });

    while let Some(value) = stream.next().await {
        println!("Processed: {}", value);
    }
}

Filter and map:

use tokio_stream::{self as stream, StreamExt};

#[tokio::main]
async fn main() {
    let mut stream = stream::iter(1..=10)
        .filter(|x| *x % 2 == 0)
        .map(|x| x * 2);

    while let Some(value) = stream.next().await {
        println!("{}", value);
    }
}

Problem: Async TCP Server

Scenario

You need to build a concurrent TCP server.

Solution: Use tokio::net::TcpListener

use tokio::net::TcpListener;
use tokio::io::{AsyncReadExt, AsyncWriteExt};

#[tokio::main]
async fn main() -> std::io::Result<()> {
    let listener = TcpListener::bind("127.0.0.1:8080").await?;
    println!("Server listening on 127.0.0.1:8080");

    loop {
        let (mut socket, addr) = listener.accept().await?;
        println!("Connection from: {}", addr);

        tokio::spawn(async move {
            let mut buf = vec![0; 1024];

            match socket.read(&mut buf).await {
                Ok(n) => {
                    println!("Received {} bytes", n);
                    socket.write_all(&buf[0..n]).await.unwrap();
                }
                Err(e) => eprintln!("Error: {}", e),
            }
        });
    }
}

Problem: Async Database Queries

Scenario

You need to query a database asynchronously.

Solution: Use sqlx

[dependencies]
sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "sqlite"] }
tokio = { version = "1.0", features = ["full"] }
use sqlx::sqlite::SqlitePool;

#[derive(sqlx::FromRow)]
struct User {
    id: i64,
    name: String,
}

#[tokio::main]
async fn main() -> Result<(), sqlx::Error> {
    let pool = SqlitePool::connect("sqlite::memory:").await?;

    // Create table
    sqlx::query("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)")
        .execute(&pool)
        .await?;

    // Insert
    sqlx::query("INSERT INTO users (name) VALUES (?)")
        .bind("Alice")
        .execute(&pool)
        .await?;

    // Query
    let users: Vec<User> = sqlx::query_as("SELECT * FROM users")
        .fetch_all(&pool)
        .await?;

    for user in users {
        println!("User {}: {}", user.id, user.name);
    }

    Ok(())
}

Problem: Choosing Between Tokio and async-std

Scenario

You’re unsure which async runtime to use.

Comparison

Tokio:

  • More popular, larger ecosystem
  • Better performance for I/O-heavy workloads
  • More features (tracing, metrics)
  • Recommended for production

async-std:

  • Closer to standard library API
  • Simpler API surface
  • Good for learning async Rust

Recommendation: Use Tokio for most projects.


Common Pitfalls

Pitfall 1: Blocking in Async Code

Problem: Calling blocking code in async context.

// Bad - blocks the async runtime
#[tokio::main]
async fn main() {
    std::thread::sleep(Duration::from_secs(5));  // Blocks entire runtime
}

Solution: Use async equivalents or spawn_blocking.

// Good
#[tokio::main]
async fn main() {
    tokio::time::sleep(Duration::from_secs(5)).await;  // Async sleep
}

// Or for blocking operations
#[tokio::main]
async fn main() {
    tokio::task::spawn_blocking(|| {
        // Expensive blocking operation
        std::thread::sleep(Duration::from_secs(5));
    }).await.unwrap();
}

Pitfall 2: Not Awaiting Futures

Problem: Creating future but not awaiting it.

// Bad - future is created but never executed
async fn work() {
    println!("Working");
}

#[tokio::main]
async fn main() {
    work();  // Future created but not awaited - doesn't run!
}

Solution: Always await futures.

#[tokio::main]
async fn main() {
    work().await;  // Now it runs
}

Pitfall 3: Deadlock with Mutex

Problem: Holding std::sync::Mutex across await points.

// Bad - potential deadlock
let data = mutex.lock().unwrap();
async_operation().await;
*data = new_value;

Solution: Use tokio::sync::Mutex or release lock before await.

// Good
let data = tokio_mutex.lock().await;
*data = new_value;

Related Resources


Write efficient async Rust code with confidence!

Last updated