Sling Academy
Home/Rust/Migrating Synchronous Code to async Functions in Rust

Migrating Synchronous Code to async Functions in Rust

Last updated: January 03, 2025

Migrating your Rust codebase from synchronous operations to asynchronous functions can enhance performance and responsiveness, especially when dealing with I/O-bound tasks. Rust’s async-await feature allows tasks to be non-blocking and run concurrently, providing improvements to a system’s efficiency and throughput.

Understanding Asynchrony in Rust

At its core, asynchronous programming involves running tasks that can pause and resume without blocking the entire application. In Rust, this is achieved through the async and await keywords, supported by the futures crate and async runtimes like tokio or async-std.

Converting a Function to Async

To start converting your synchronous code to async, identify functions that rely heavily on I/O operations, such as network requests and file system interactions. Consider a simple synchronous function fetching data from a URL:

use reqwest; 

fn fetch_data(url: &str) -> Result<String, reqwest::Error> {
    let response = reqwest::blocking::get(url)?;
    let body = response.text()?;
    Ok(body)
}

In this synchronous example, the reqwest::blocking::get operation blocks the current thread while waiting for the response. To convert it to an asynchronous function, follow these steps:

use reqwest::Client;

async fn fetch_data_async(url: &str) -> Result<String, reqwest::Error> {
    let client = Client::new();
    let response = client.get(url).send().await?;
    let body = response.text().await?;
    Ok(body)
}

In the async variation, the send() and text() methods are awaited, ensuring the tasks yield while waiting, thus freeing up the thread to do other useful work.

Choosing an Async Runtime

Unlike some languages, asynchronous functions in Rust don't automatically run concurrently. You must choose a runtime like tokio or async-std to execute your async functions.

Async-std Example

First, add async-std to your Cargo.toml:

[dependencies]
async-std = "1.10.0"

Then, create a main function to execute our async task:

#[async_std::main]
async fn main() {
    match fetch_data_async("https://example.com").await {
        Ok(content) => println!("Content retrieved successfully: {}", content),
        Err(e) => eprintln!("Error fetching data: {}", e),
    }
}

Using Tokio

Alternatively, to use tokio, add it to your dependencies:

[dependencies]
tokio = { version = "1", features = ["full"] }
reqwest = { version = "0.11", features = ["json"] }

Here’s how you can execute your asynchronous function with tokio:

#[tokio::main]
async fn main() {
    match fetch_data_async("https://example.com").await {
        Ok(content) => println!("Content: {}", content),
        Err(e) => eprintln!("Error: {}", e),
    }
}

Handling Errors in Async Code

Error handling in async Rust involves using the same principles as synchronous code: employing the Result type or the ? operator. It’s essential to handle errors at every await point, given that I/O operations are error-prone.

Conclusion

Transitioning to async in Rust can result in more performant applications by leveraging concurrent task execution for I/O-bound operations. A successful migration involves updating dependencies, modifying function signatures, implementing an async runtime, and pay attention to error handling. Though it might seem complex, the gains in resource efficiency can significantly offset the migration costs.

Next Article: Trait Objects vs Generics for Function Return Types

Previous Article: Ownership Strategies in Function Calls: Move, Borrow, Copy

Series: Working with Functions in Rust

Rust

You May Also Like

  • E0557 in Rust: Feature Has Been Removed or Is Unavailable in the Stable Channel
  • Network Protocol Handling Concurrency in Rust with async/await
  • Using the anyhow and thiserror Crates for Better Rust Error Tests
  • Rust - Investigating partial moves when pattern matching on vector or HashMap elements
  • Rust - Handling nested or hierarchical HashMaps for complex data relationships
  • Rust - Combining multiple HashMaps by merging keys and values
  • Composing Functionality in Rust Through Multiple Trait Bounds
  • E0437 in Rust: Unexpected `#` in macro invocation or attribute
  • Integrating I/O and Networking in Rust’s Async Concurrency
  • E0178 in Rust: Conflicting implementations of the same trait for a type
  • Utilizing a Reactor Pattern in Rust for Event-Driven Architectures
  • Parallelizing CPU-Intensive Work with Rust’s rayon Crate
  • Managing WebSocket Connections in Rust for Real-Time Apps
  • Downloading Files in Rust via HTTP for CLI Tools
  • Mocking Network Calls in Rust Tests with the surf or reqwest Crates
  • Rust - Designing advanced concurrency abstractions using generic channels or locks
  • Managing code expansion in debug builds with heavy usage of generics in Rust
  • Implementing parse-from-string logic for generic numeric types in Rust
  • Rust.- Refining trait bounds at implementation time for more specialized behavior