| name | rust-async-patterns |
| description | Master Rust async programming with Tokio, async traits, error handling, and concurrent patterns. Use when building async Rust applications, implementing concurrent systems, or debugging async code. |
Rust Async Patterns
Production patterns for async Rust programming with Tokio runtime, including tasks, channels, streams, and error handling.
When to Use This Skill
- Building async Rust applications
- Implementing concurrent network services
- Using Tokio for async I/O
- Handling async errors properly
- Debugging async code issues
- Optimizing async performance
Core Concepts
1. Async Execution Model
Future (lazy) → poll() → Ready(value) | Pending
↑ ↓
Waker ← Runtime schedules
2. Key Abstractions
| Concept | Purpose |
|---|
Future | Lazy computation that may complete later |
async fn | Function returning impl Future |
await | Suspend until future completes |
Task | Spawned future running concurrently |
Runtime | Executor that polls futures |
Quick Start
[dependencies]
tokio = { version = "1", features = ["full"] }
futures = "0.3"
async-trait = "0.1"
anyhow = "1.0"
tracing = "0.1"
tracing-subscriber = "0.3"
use tokio::time::{sleep, Duration};
use anyhow::Result;
#[tokio::main]
async fn main() -> Result<()> {
tracing_subscriber::fmt::init();
let result = fetch_data("https://api.example.com").await?;
println!("Got: {}", result);
Ok(())
}
async fn fetch_data(url: &str) -> Result<String> {
sleep(Duration::from_millis(100)).await;
Ok(format!("Data from {}", url))
}
Patterns
Pattern 1: Concurrent Task Execution
use tokio::task::JoinSet;
use anyhow::Result;
async fn fetch_all_concurrent(urls: Vec<String>) -> Result<Vec<String>> {
let mut set = JoinSet::new();
for url in urls {
set.spawn(async move {
fetch_data(&url).await
});
}
let mut results = Vec::new();
while let Some(res) = set.join_next().await {
match res {
Ok(Ok(data)) => results.push(data),
Ok(Err(e)) => tracing::error!("Task failed: {}", e),
Err(e) => tracing::error!("Join error: {}", e),
}
}
Ok(results)
}
use futures::stream::{self, StreamExt};
(urls: <>, limit: ) <<>> {
stream::(urls)
.(|url| { (&url). })
.(limit)
.()
.
}
tokio::select;
(url1: &, url2: &) <> {
{
result = (url1) => result,
result = (url2) => result,
}
}
Pattern 2: Channels for Communication
use tokio::sync::{mpsc, broadcast, oneshot, watch};
async fn mpsc_example() {
let (tx, mut rx) = mpsc::channel::<String>(100);
let tx2 = tx.clone();
tokio::spawn(async move {
tx2.send("Hello".to_string()).await.unwrap();
});
while let Some(msg) = rx.recv().await {
println!("Got: {}", msg);
}
}
async fn broadcast_example() {
let (tx, _) = broadcast::channel::<String>(100);
let mut rx1 = tx.subscribe();
let mut rx2 = tx.subscribe();
tx.send("Event".to_string()).unwrap();
= rx1.().;
= rx2.().;
}
() {
(tx, rx) = oneshot::channel::<>();
tokio::( {
tx.(.()).();
});
rx..()
}
() {
(tx, rx) = watch::(.());
tokio::( {
{
rx.()..();
(, *rx.());
}
});
tx.(.()).();
}
Pattern 3: Async Error Handling
use anyhow::{Context, Result, bail};
use thiserror::Error;
#[derive(Error, Debug)]
pub enum ServiceError {
#[error("Network error: {0}")]
Network(#[from] reqwest::Error),
#[error("Database error: {0}")]
Database(#[from] sqlx::Error),
#[error("Not found: {0}")]
NotFound(String),
#[error("Timeout after {0:?}")]
Timeout(std::time::Duration),
}
async fn process_request(id: &str) -> Result<Response> {
let data = fetch_data(id)
.await
.context("Failed to fetch data")?;
let parsed = parse_response(&data)
.context("Failed to parse response")?;
Ok(parsed)
}
async fn get_user(id: &str) -> Result<User, ServiceError> {
= db.(id).?;
result {
(user) => (user),
=> (ServiceError::(id.())),
}
}
tokio::time::timeout;
<T, F>(duration: Duration, future: F) <T, ServiceError>
F: std::future::Future<Output = <T, ServiceError>>,
{
(duration, future)
.
.(|_| ServiceError::(duration))?
}
Pattern 4: Graceful Shutdown
use tokio::signal;
use tokio::sync::broadcast;
use tokio_util::sync::CancellationToken;
async fn run_server() -> Result<()> {
let token = CancellationToken::new();
let token_clone = token.clone();
tokio::spawn(async move {
loop {
tokio::select! {
_ = token_clone.cancelled() => {
tracing::info!("Task shutting down");
break;
}
_ = do_work() => {}
}
}
});
signal::ctrl_c().await?;
tracing::info!("Shutdown signal received");
token.cancel();
tokio::time::sleep(Duration::from_secs(5)).await;
Ok(())
}
async fn run_with_broadcast() -> Result<()> {
(shutdown_tx, _) = broadcast::channel::<()>();
= shutdown_tx.();
tokio::( {
tokio:: {
_ = rx.() => {
tracing::info!();
}
_ = { { (). } } => {}
}
});
signal::().?;
= shutdown_tx.(());
(())
}
Pattern 5: Async Traits
use async_trait::async_trait;
#[async_trait]
pub trait Repository {
async fn get(&self, id: &str) -> Result<Entity>;
async fn save(&self, entity: &Entity) -> Result<()>;
async fn delete(&self, id: &str) -> Result<()>;
}
pub struct PostgresRepository {
pool: sqlx::PgPool,
}
#[async_trait]
impl Repository for PostgresRepository {
async fn get(&self, id: &str) -> Result<Entity> {
sqlx::query_as!(Entity, "SELECT * FROM entities WHERE id = $1", id)
.fetch_one(&self.pool)
.await
.map_err(Into::into)
}
async fn save(&self, entity: &Entity) -> <()> {
sqlx::query!(
,
entity.id,
entity.data
)
.(&.pool)
.?;
(())
}
(&, id: &) <()> {
sqlx::query!(, id)
.(&.pool)
.?;
(())
}
}
(repo: & Repository, id: &) <()> {
= repo.(id).?;
repo.(&entity).
}
Pattern 6: Streams and Async Iteration
use futures::stream::{self, Stream, StreamExt};
use async_stream::stream;
fn numbers_stream() -> impl Stream<Item = i32> {
stream! {
for i in 0..10 {
tokio::time::sleep(Duration::from_millis(100)).await;
yield i;
}
}
}
async fn process_stream() {
let stream = numbers_stream();
let processed: Vec<_> = stream
.filter(|n| futures::future::ready(*n % 2 == 0))
.map(|n| n * 2)
.collect()
.await;
println!("{:?}", processed);
}
async fn process_in_chunks() {
let stream = numbers_stream();
= stream.();
(chunk) = chunks.(). {
(, chunk);
}
}
() {
= ();
= ();
= stream::(stream1, stream2);
merged
.for_each(|n| {
(, n);
})
.;
}
Pattern 7: Resource Management
use std::sync::Arc;
use tokio::sync::{Mutex, RwLock, Semaphore};
struct Cache {
data: RwLock<HashMap<String, String>>,
}
impl Cache {
async fn get(&self, key: &str) -> Option<String> {
self.data.read().await.get(key).cloned()
}
async fn set(&self, key: String, value: String) {
self.data.write().await.insert(key, value);
}
}
struct Pool {
semaphore: Semaphore,
connections: Mutex<Vec<Connection>>,
}
impl Pool {
fn new(size: usize) -> Self {
Self {
semaphore: Semaphore::new(size),
connections: Mutex::new((0..size).map(|_| Connection::()).()),
}
}
(&) PooledConnection<> {
= .semaphore.()..();
= .connections.()..().();
PooledConnection { pool: , conn: (conn), _permit: permit }
}
}
<> {
pool: & Pool,
conn: <Connection>,
_permit: tokio::sync::SemaphorePermit<>,
}
<> {
(& ) {
(conn) = .conn.() {
= .pool;
tokio::( {
pool.connections.()..(conn);
});
}
}
}
Debugging Tips
use tracing::instrument;
#[instrument(skip(pool))]
async fn fetch_user(pool: &PgPool, id: &str) -> Result<User> {
tracing::debug!("Fetching user");
}
let span = tracing::info_span!("worker", id = %worker_id);
tokio::spawn(async move {
}.instrument(span));
Best Practices
Do's
- Use
tokio::select! - For racing futures
- Prefer channels - Over shared state when possible
- Use
JoinSet - For managing multiple tasks
- Instrument with tracing - For debugging async code
- Handle cancellation - Check
CancellationToken
Don'ts
- Don't block - Never use
std::thread::sleep in async
- Don't hold locks across awaits - Causes deadlocks
- Don't spawn unboundedly - Use semaphores for limits
- Don't ignore errors - Propagate with
? or log
- Don't forget Send bounds - For spawned futures
Resources