DEV Community

이관호(Gwanho LEE)
이관호(Gwanho LEE)

Posted on

Understanding Async/Await in Rust: A Simple Guide

Rust, Async Programming


What is Async/Await?

Async/await is a way to write code that can wait for things (like internet requests) without blocking your entire program.

Think of it like waiting in line at a restaurant:

Without Async (Blocking)

fn order_food() {
    println!("1. Go to restaurant");
    println!("2. Wait in line...");  // ← You can't do anything else here!
    println!("3. Order food");
    println!("4. Wait for food..."); // ← You can't do anything else here!
    println!("5. Get food");
}
Enter fullscreen mode Exit fullscreen mode

With Async (Non-blocking)

async fn order_food() {
    println!("1. Go to restaurant");

    // While waiting in line, you can check your phone
    let line_task = wait_in_line().await;  // ← "I'll wait, but don't stop everything"

    println!("2. Order food");

    // While food cooks, you can read a book
    let food_task = cook_food().await;     // ← "I'll wait, but don't stop everything"

    println!("3. Get food");
}
Enter fullscreen mode Exit fullscreen mode

How Async Works in Rust

1. Async Functions Return "Futures"

// This function returns a "promise" of a String, not a String
async fn fetch_data() -> String {
    // Simulate waiting for internet
    tokio::time::sleep(Duration::from_secs(2)).await;
    "Hello from internet!".to_string()
}

// Usage
async fn main() {
    let future = fetch_data();  // ← This doesn't run yet!
    println!("Future created, but not executed");

    let result = future.await;  // ← Now it actually runs and waits
    println!("Got result: {}", result);
}
Enter fullscreen mode Exit fullscreen mode

2. The Magic of .await

async fn example() {
    println!("Starting...");

    // Create multiple futures
    let future1 = fetch_data_1();  // ← Just creates a plan
    let future2 = fetch_data_2();  // ← Just creates a plan

    println!("Both tasks planned!");

    // Now actually wait for results
    let result1 = future1.await;  // ← Actually does the work
    let result2 = future2.await;  // ← Actually does the work

    println!("Both done!");
}
Enter fullscreen mode Exit fullscreen mode

Real Example: Your Bitcoin Wallet

In your Bitcoin wallet, you use async when talking to the internet:

async fn update_balances(&mut self) -> Result<(), Box<dyn StdError>> {
    for address_info in &mut self.addresses {
        // This could take 1-5 seconds to get response from internet
        let response = client.get(&url).send().await?;  // ← Wait for internet

        if response.status().is_success() {
            // This could also take time
            let utxos: Vec<UTXO> = response.json().await?;  // ← Wait for JSON parsing

            address_info.utxos = utxos;
        }
    }
    Ok(())
}
Enter fullscreen mode Exit fullscreen mode

Why Use Async?

1. Better Performance

// Without async - takes 6 seconds total
fn fetch_three_things() {
    fetch_thing_1();  // 2 seconds
    fetch_thing_2();  // 2 seconds  
    fetch_thing_3();  // 2 seconds
    // Total: 6 seconds
}

// With async - takes 2 seconds total!
async fn fetch_three_things() {
    let future1 = fetch_thing_1();  // Start all at once
    let future2 = fetch_thing_2();  // Start all at once
    let future3 = fetch_thing_3();  // Start all at once

    // Wait for all to complete
    let (result1, result2, result3) = tokio::join!(future1, future2, future3);
    // Total: 2 seconds (all run in parallel)
}
Enter fullscreen mode Exit fullscreen mode

2. Better User Experience

// Without async - UI freezes while loading
fn load_profile() {
    let data = fetch_from_internet();  // UI freezes here
    update_ui(data);
}

// With async - UI stays responsive
async fn load_profile() {
    let data = fetch_from_internet().await;  // UI doesn't freeze
    update_ui(data);
}
Enter fullscreen mode Exit fullscreen mode

Simple Rules

When to Use await:

  • ✅ Making internet requests (like your Bitcoin API calls)
  • ✅ Reading/writing files
  • ✅ Database operations
  • ✅ Any operation that might take time

When NOT to Use await:

  • ❌ Simple calculations
  • ❌ Working with memory data
  • ❌ Operations that are always instant

Common Patterns

1. Multiple Async Operations

async fn fetch_user_data(user_id: u32) -> Result<User, Error> {
    // Start both requests at the same time
    let profile_future = fetch_profile(user_id);
    let settings_future = fetch_settings(user_id);

    // Wait for both to complete
    let (profile, settings) = tokio::join!(profile_future, settings_future);

    Ok(User { profile, settings })
}
Enter fullscreen mode Exit fullscreen mode

2. Error Handling

async fn safe_operation() -> Result<String, Error> {
    match fetch_data().await {
        Ok(data) => Ok(data),
        Err(e) => {
            println!("Error: {}", e);
            Err(e)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

3. Timeouts

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

async fn fetch_with_timeout() -> Result<String, Error> {
    // Wait max 5 seconds
    match timeout(Duration::from_secs(5), fetch_data()).await {
        Ok(data) => Ok(data),
        Err(_) => Err(Error::Timeout),
    }
}
Enter fullscreen mode Exit fullscreen mode

Testing Async Code

#[cfg(test)]
mod tests {
    use super::*;
    use tokio::test;  // ← Special test macro for async

    #[test]
    async fn test_fetch_data() {
        let result = fetch_data().await;
        assert_eq!(result, "Hello from internet!");
    }
}
Enter fullscreen mode Exit fullscreen mode

Your Bitcoin Wallet Example

Here's how async makes your wallet better:

// Before: Sequential (slow)
async fn update_wallet_slow(&mut self) {
    for address in &self.addresses {
        let utxos = fetch_utxos(address).await;  // Wait for each one
        // This takes: 3 addresses × 2 seconds = 6 seconds total
    }
}

// After: Parallel (fast)
async fn update_wallet_fast(&mut self) {
    let mut futures = Vec::new();

    // Start all requests at once
    for address in &self.addresses {
        let future = fetch_utxos(address);
        futures.push(future);
    }

    // Wait for all to complete
    let results = futures::future::join_all(futures).await;
    // This takes: 2 seconds total (all run in parallel)
}
Enter fullscreen mode Exit fullscreen mode

Key Takeaways

  1. async fn creates a "plan" (Future), doesn't execute immediately
  2. .await actually executes the plan and waits for result
  3. Multiple async operations can run in parallel
  4. Better performance - don't block while waiting
  5. Better user experience - UI stays responsive

Simple Mental Model

Think of async/await like cooking multiple dishes:

async fn cook_dinner() {
    // Start all dishes at once
    let pasta_future = cook_pasta();      // 10 minutes
    let sauce_future = cook_sauce();      // 8 minutes
    let salad_future = make_salad();      // 5 minutes

    // Wait for all to be ready
    let (pasta, sauce, salad) = tokio::join!(pasta_future, sauce_future, salad_future);

    // Dinner ready in 10 minutes (not 23 minutes!)
}
Enter fullscreen mode Exit fullscreen mode

That's it! Async/await lets you do multiple things at once instead of one after another. Perfect for internet requests, file operations, and anything that takes time.


This guide covered the basics of async/await in Rust. For more advanced topics, check out the Rust async book and Tokio documentation.

Tags: #rust #async #beginners #programming

Top comments (0)