- Introduction to Asynchronous Programming in Rust
- Understanding the Future Trait
- Basic Examples: Your First Futures
- Implementing Custom Futures
- Working with Combinators
- Error Handling in Async Code
- Real-World Examples
- Advanced Patterns and Best Practices
- Testing Async Code
- Performance Considerations
- Common Pitfalls and How to Avoid Them
Asynchronous programming in Rust allows you to write concurrent code that can handle multiple tasks efficiently without blocking threads. At the heart of Rust's async ecosystem is the Future trait, which represents a computation that will complete at some point in the future.
- Futures are lazy: They don't do any work until polled
- Cooperative multitasking: Tasks yield control voluntarily at
.awaitpoints - Zero-cost abstractions: Async/await compiles to efficient state machines
- Memory safety: Pin ensures self-referential futures remain valid
// Synchronous code - blocks the thread
fn sync_example() {
let data1 = fetch_data_from_api(); // Blocks for 100ms
let data2 = fetch_data_from_db(); // Blocks for 200ms
// Total time: ~300ms
}
// Asynchronous code - concurrent execution
async fn async_example() {
let future1 = fetch_data_from_api(); // Returns immediately
let future2 = fetch_data_from_db(); // Returns immediately
let (data1, data2) = tokio::join!(future1, future2);
// Total time: ~200ms (max of both operations)
}The Future trait is the foundation of async programming in Rust:
pub trait Future {
type Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}
pub enum Poll<T> {
Ready(T), // The future has completed
Pending, // The future is not ready yet
}- Output: The type of value the future will produce
- poll(): Method called by the executor to advance the future
- Pin: Ensures the future doesn't move in memory
- Context: Provides access to the Waker for efficient scheduling
Let's start with simple examples to understand how futures work.
What you'll learn:
- How to define and use async functions in Rust.
- The difference between sequential and concurrent execution.
- Why futures are "lazy" and only do work when awaited.
Example: Basic async/await
// An async function that simulates work by sleeping for a given duration.
async fn simple_async_operation(duration: Duration, message: &str) -> String {
println!("Starting: {}", message);
sleep(duration).await; // Non-blocking sleep
format!("{} completed!", message)
}
// Running multiple async operations sequentially:
let result1 = simple_async_operation(Duration::from_millis(100), "Task 1").await;
let result2 = simple_async_operation(Duration::from_millis(200), "Task 2").await;
// Running them concurrently with tokio::join!:
let (result1, result2) = tokio::join!(
simple_async_operation(Duration::from_millis(100), "Task 1"),
simple_async_operation(Duration::from_millis(200), "Task 2")
);How to run:
cargo run --bin basic_futureFile: src/examples/poll_states.rs
This example shows how the Poll enum works and when futures return Ready vs Pending.
File: src/examples/join_example.rs
This example demonstrates concurrent execution of multiple futures.
What you'll learn:
- How to implement the
Futuretrait manually. - How to use
Wakerand shared state for efficient async scheduling. - The principle of "lazy" futures: work starts only when polled.
Example: Custom Delay Future
// A custom future that completes after a delay.
pub struct DelayFuture {
shared_state: Arc<Mutex<SharedState>>,
duration: Duration,
started: bool,
}
impl Future for DelayFuture {
type Output = String;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let shared_state = self.shared_state.lock().unwrap();
if shared_state.completed {
return Poll::Ready(format!("Delay of {:?} completed!", self.duration));
}
drop(shared_state);
self.start_timer(); // Spawns a thread to wait and wake
let mut shared_state = self.shared_state.lock().unwrap();
shared_state.waker = Some(cx.waker().clone());
Poll::Pending
}
}How to run:
cargo run --bin custom_delayFile: src/examples/custom_join.rs
Implementation of a custom combinator that runs multiple futures concurrently.
What you'll learn:
- How to use combinators like
map,and_then,join!, andselect!to compose async workflows. - How to handle errors and transform results in async chains.
- How to run collections of futures concurrently.
Example: Using map and join! combinators
// Transform the output of a future using map
let result = simulate_database_query("users", Duration::from_millis(100))
.map(|data| format!("TRANSFORMED: {}", data.to_uppercase()))
.await;
// Run multiple async operations concurrently with join!
let (api_result, db_result) = tokio::join!(
simulate_api_call("orders", Duration::from_millis(150), true),
simulate_database_query("inventory", Duration::from_millis(200))
);How to run:
cargo run --bin combinatorsWhat you'll learn:
- How to use
Result<T, E>in async functions. - How to propagate errors with
?and handle them with pattern matching. - How to define and use custom error types with
thiserror.
Example: Custom error handling in async Rust
#[derive(Error, Debug, Clone)]
pub enum ApiError {
#[error("Network error: {message}")]
NetworkError { message: String },
#[error("Authentication failed: {reason}")]
AuthenticationError { reason: String },
// ... other variants ...
}
async fn simulate_api_request(endpoint: &str, should_succeed: bool) -> Result<String, ApiError> {
if should_succeed {
Ok(format!("API response from '{}' endpoint", endpoint))
} else {
Err(ApiError::NetworkError { message: "Failed to connect".to_string() })
}
}
// Handling errors with pattern matching
match simulate_api_request("users", false).await {
Ok(data) => println!("Success: {}", data),
Err(ApiError::NetworkError { message }) => println!("Network failed: {}", message),
Err(e) => println!("Other error: {}", e),
}How to run:
cargo run --bin error_handlingWhat you'll learn:
- How to use async HTTP clients (
reqwest) and JSON serialization (serde). - How to aggregate data from multiple async sources.
- How to implement caching, rate limiting, and error resilience in real-world async code.
Example: Making an async HTTP request with caching
let client = ApiClient::new("https://jsonplaceholder.typicode.com");
let users = client.get_users().await?;
// ApiClient uses caching and rate limiting internally
let posts = client.get_user_posts(1).await?;How to run:
cargo run --bin real_worldFile: src/examples/advanced_patterns.rs
Advanced techniques including:
- Custom executors
- Stream processing
- Backpressure handling
- Cancellation-safe futures
- Performance optimization
File: src/tests/async_tests.rs
Comprehensive testing examples:
- Unit testing async functions
- Integration testing
- Mocking async dependencies
- Testing error conditions
- Performance testing
- When to use
spawn()vs direct.await - Memory allocation patterns
- CPU vs I/O bound tasks
- Choosing the right runtime
// ❌ Wrong - future is created but never executed
async fn wrong_example() {
fetch_data(); // This does nothing!
}
// ✅ Correct - future is awaited
async fn correct_example() {
fetch_data().await; // This executes the future
}// ❌ Wrong - blocks the async runtime
async fn wrong_blocking() {
std::thread::sleep(Duration::from_secs(1)); // Blocks!
}
// ✅ Correct - uses async sleep
async fn correct_async() {
tokio::time::sleep(Duration::from_secs(1)).await;
}// ❌ Wrong - Rc is not Send
async fn wrong_send() {
let data = Rc::new(42);
some_async_operation().await;
println!("{}", data); // Error: Rc held across await
}
// ✅ Correct - Arc is Send + Sync
async fn correct_send() {
let data = Arc::new(42);
some_async_operation().await;
println!("{}", data); // Works!
}Each example can be run individually:
# Basic future example
cargo run --bin basic_future
# Custom delay implementation
cargo run --bin custom_delay
# Combinators example
cargo run --bin combinators
# Error handling patterns
cargo run --bin error_handling
# Real-world HTTP client
cargo run --bin real_world
# Advanced patterns
cargo run --bin advanced_patternsRun the complete test suite:
# Run all tests
cargo test
# Run with output
cargo test -- --nocapture
# Run specific test
cargo test test_custom_delayAfter completing this tutorial, you should:
- Understand the Future trait and its components
- Be able to implement custom futures when needed
- Know how to use combinators effectively
- Handle errors properly in async code
- Avoid common pitfalls
- Write testable async code
This tutorial provides hands-on, tested examples for learning the Future trait in Rust. All code examples are verified to compile and run correctly.