Files
gh-emillindfors-claude-mark…/commands/rust-async-traits.md
2025-11-29 18:25:55 +08:00

9.1 KiB

description
description
Modernize async trait usage to native async fn in traits

You are helping modernize async trait definitions to use native async fn in traits instead of the async-trait crate.

Your Task

Convert traits using the async-trait crate to native async fn in traits, which has been supported since Rust 1.75.

Background

Since Rust 1.75 (December 2023): Async functions in traits are natively supported without requiring the async-trait crate.

When to use async-trait:

  • Supporting Rust versions < 1.75
  • Need for dyn Trait (dynamic dispatch/object safety)
  • Specific edge cases with complex bounds

When to use native async fn:

  • Rust 1.75 or later
  • Static dispatch (generics)
  • Modern codebases

Steps

  1. Check Current Usage

    Scan for async-trait usage:

    use async_trait::async_trait;
    
    #[async_trait]
    trait MyTrait {
        async fn method(&self) -> Result<T, E>;
    }
    
  2. Verify Rust Version

    Check Cargo.toml:

    [package]
    rust-version = "1.75"  # Or higher
    

    If rust-version < 1.75, ask user if they can upgrade.

  3. Identify Use Cases

    Categorize each async trait:

    Can Remove async-trait (most common):

    • Trait used with generics/static dispatch
    • No Box<dyn Trait> usage
    • Rust 1.75+

    Must Keep async-trait:

    • Using dyn Trait for dynamic dispatch
    • Supporting older Rust versions
    • Object safety required
  4. Convert to Native Async Fn

    Before:

    use async_trait::async_trait;
    
    #[async_trait]
    pub trait UserRepository: Send + Sync {
        async fn find_user(&self, id: &str) -> Result<User, Error>;
        async fn save_user(&self, user: &User) -> Result<(), Error>;
        async fn delete_user(&self, id: &str) -> Result<(), Error>;
    }
    
    #[async_trait]
    impl UserRepository for PostgresRepository {
        async fn find_user(&self, id: &str) -> Result<User, Error> {
            sqlx::query_as!(User, "SELECT * FROM users WHERE id = $1", id)
                .fetch_one(&self.pool)
                .await
        }
    
        async fn save_user(&self, user: &User) -> Result<(), Error> {
            sqlx::query!(
                "INSERT INTO users (id, email) VALUES ($1, $2)",
                user.id,
                user.email
            )
            .execute(&self.pool)
            .await?;
            Ok(())
        }
    
        async fn delete_user(&self, id: &str) -> Result<(), Error> {
            sqlx::query!("DELETE FROM users WHERE id = $1", id)
                .execute(&self.pool)
                .await?;
            Ok(())
        }
    }
    

    After:

    // No async_trait import needed!
    
    pub trait UserRepository: Send + Sync {
        async fn find_user(&self, id: &str) -> Result<User, Error>;
        async fn save_user(&self, user: &User) -> Result<(), Error>;
        async fn delete_user(&self, id: &str) -> Result<(), Error>;
    }
    
    impl UserRepository for PostgresRepository {
        async fn find_user(&self, id: &str) -> Result<User, Error> {
            sqlx::query_as!(User, "SELECT * FROM users WHERE id = $1", id)
                .fetch_one(&self.pool)
                .await
        }
    
        async fn save_user(&self, user: &User) -> Result<(), Error> {
            sqlx::query!(
                "INSERT INTO users (id, email) VALUES ($1, $2)",
                user.id,
                user.email
            )
            .execute(&self.pool)
            .await?;
            Ok(())
        }
    
        async fn delete_user(&self, id: &str) -> Result<(), Error> {
            sqlx::query!("DELETE FROM users WHERE id = $1", id)
                .execute(&self.pool)
                .await?;
            Ok(())
        }
    }
    
  5. Handle Dynamic Dispatch Cases

    If you need dyn Trait, keep async-trait:

    // When you need this:
    let repo: Box<dyn UserRepository> = Box::new(PostgresRepository::new(pool));
    
    // You MUST use async-trait for object safety:
    use async_trait::async_trait;
    
    #[async_trait]
    pub trait UserRepository: Send + Sync {
        async fn find_user(&self, id: &str) -> Result<User, Error>;
    }
    

    Or redesign to avoid dynamic dispatch:

    // Alternative: Use generics instead
    pub async fn process_users<R: UserRepository>(repo: R) {
        // Works with any UserRepository implementation
        let user = repo.find_user("123").await.unwrap();
    }
    
  6. Remove async-trait Dependency

    Update Cargo.toml:

    Before:

    [dependencies]
    async-trait = "0.1"
    

    After:

    [dependencies]
    # async-trait removed - using native async fn in traits
    

    If still needed for some traits:

    [dependencies]
    # Only needed for dyn Trait support
    async-trait = "0.1"  # Optional, only for object-safe traits
    
  7. Update Imports

    Remove unused async-trait imports:

    // Remove this if no longer needed
    use async_trait::async_trait;
    
  8. Run Tests

    Verify everything compiles and works:

    cargo check
    cargo test
    cargo clippy
    
  9. Provide Summary

    ✅ Modernized async trait usage!
    
    ## Changes Made:
    
    ### Converted to Native Async Fn (3 traits):
    - UserRepository (src/domain/user.rs)
    - OrderRepository (src/domain/order.rs)
    - PaymentGateway (src/ports/payment.rs)
    
    ### Kept async-trait (1 trait):
    - DynamicHandler (src/handlers/mod.rs)
      Reason: Uses Box<dyn Trait> for plugin system
    
    ## Dependency Updates:
    - Removed async-trait from main dependencies
    - Added as optional for dynamic dispatch cases
    
    ## Benefits:
    - ✅ Zero-cost abstraction (no boxing overhead)
    - ✅ Simpler code (no macro needed)
    - ✅ Better error messages
    - ✅ Native language feature
    
    ## Before/After Example:
    
    Before:
    ```rust
    use async_trait::async_trait;
    
    #[async_trait]
    trait Repository {
        async fn find(&self, id: &str) -> Result<Item, Error>;
    }
    
    #[async_trait]
    impl Repository for MyRepo {
        async fn find(&self, id: &str) -> Result<Item, Error> {
            // ...
        }
    }
    

    After:

    // No import needed!
    
    trait Repository {
        async fn find(&self, id: &str) -> Result<Item, Error>;
    }
    
    impl Repository for MyRepo {
        async fn find(&self, id: &str) -> Result<Item, Error> {
            // ...
        }
    }
    

    Next Steps:

    1. All tests passing
    2. Consider removing async-trait entirely if unused
    3. Update documentation
    
    

Key Differences

Native Async Fn (Rust 1.75+)

Pros:

  • No external dependency
  • Zero-cost abstraction
  • Better compiler errors
  • Simpler syntax
  • Native language feature

Cons:

  • Cannot use with dyn Trait directly
  • Requires Rust 1.75+

Usage:

trait MyTrait {
    async fn method(&self) -> Result<T, E>;
}

// Use with generics
fn process<T: MyTrait>(t: T) { }

Async-Trait Crate

Pros:

  • Works with older Rust
  • Supports dyn Trait
  • Object-safe traits

Cons:

  • External dependency
  • Macro overhead
  • Slight performance cost (boxing)

Usage:

use async_trait::async_trait;

#[async_trait]
trait MyTrait {
    async fn method(&self) -> Result<T, E>;
}

// Can use with dyn
let t: Box<dyn MyTrait> = Box::new(impl);

Migration Patterns

Pattern 1: Simple Repository

// Before
#[async_trait]
trait Repository {
    async fn get(&self, id: i32) -> Option<Item>;
}

// After
trait Repository {
    async fn get(&self, id: i32) -> Option<Item>;
}

Pattern 2: Generic Service

// Before
#[async_trait]
trait Service<T> {
    async fn process(&self, item: T) -> Result<(), Error>;
}

// After
trait Service<T> {
    async fn process(&self, item: T) -> Result<(), Error>;
}

Pattern 3: Multiple Async Methods

// Before
#[async_trait]
trait Complex {
    async fn fetch(&self) -> Result<Data, Error>;
    async fn save(&self, data: Data) -> Result<(), Error>;
    async fn delete(&self, id: i32) -> Result<(), Error>;
}

// After - just remove the macro!
trait Complex {
    async fn fetch(&self) -> Result<Data, Error>;
    async fn save(&self, data: Data) -> Result<(), Error>;
    async fn delete(&self, id: i32) -> Result<(), Error>;
}

Pattern 4: Keep for Dynamic Dispatch

// When you need this:
struct PluginSystem {
    plugins: Vec<Box<dyn Plugin>>,
}

// Keep async-trait:
#[async_trait]
trait Plugin: Send + Sync {
    async fn execute(&self) -> Result<(), Error>;
}

Important Notes

  • Native async fn in traits requires Rust 1.75+
  • Check MSRV before removing async-trait
  • dyn Trait requires async-trait (or alternatives)
  • Static dispatch (generics) works with native async fn
  • Performance is better with native async fn (no boxing)

Version Requirements

Feature Rust Version Notes
Async fn in traits 1.75.0+ Native support
async-trait crate Any Fallback for older versions
Return-position impl Trait 1.75.0+ Enables async fn

After Completion

Ask the user:

  1. Did all tests pass?
  2. Can we remove async-trait entirely?
  3. Are there any dyn Trait use cases remaining?
  4. Should we update documentation?