2HAASStart a project
All posts

Tech News

Rust async functions in traits: what changed and why

Rust 1.75 made async fn in traits part of stable Rust, removing the need for the async-trait macro in many cases. What you can write now and the limits that remain.

The 2HAAS team2 min read

For years, one line of Rust refused to compile: an async fn inside a trait. Traits are how Rust expresses shared behaviour, and async is how it handles network work, so nearly every service ran into the gap. The usual answer was the async-trait crate, a macro that rewrote the function to return a boxed future.

Rust 1.75, released in December 2023, made async fn in traits part of the stable language. You will sometimes see version 1.77 quoted for this. The feature landed in 1.75.

Before and after

With the macro:

use async_trait::async_trait;

#[async_trait]
trait Fetcher {
    async fn fetch(&self, url: &str) -> Result<String, FetchError>;
}

With stable Rust today:

trait Fetcher {
    async fn fetch(&self, url: &str) -> Result<String, FetchError>;
}

struct HttpFetcher;

impl Fetcher for HttpFetcher {
    async fn fetch(&self, url: &str) -> Result<String, FetchError> {
        // call the network here
        todo!()
    }
}

async fn load<F: Fetcher>(fetcher: &F) -> Result<String, FetchError> {
    fetcher.fetch("https://example.com/data").await
}

No macro and no extra dependency. There is also no heap allocation per call, because the future is no longer boxed.

Why it took so long

An async fn returns an anonymous future type that the compiler generates. In a trait, each implementation produces a different one. The language first needed a way for a trait method to return "some type that implements this trait". Rust 1.75 stabilized that as well, and async fn in traits is built on top of it.

The limits that remain

No dynamic dispatch. A trait with an async fn cannot be used as dyn Fetcher. If you need a list of different fetchers behind Box<dyn Fetcher>, the async-trait crate is still the practical choice.

Send is not guaranteed. Multi-threaded runtimes such as Tokio need futures that can move between threads. A generic function cannot assume the future returned by a trait method is Send. This is why the compiler warns when a public trait contains an async fn.

For libraries, the Rust project maintains the trait-variant crate, which generates a Send version of a trait from one definition:

#[trait_variant::make(Fetcher: Send)]
trait LocalFetcher {
    async fn fetch(&self, url: &str) -> Result<String, FetchError>;
}

What to do in practice

Situation Recommendation
A trait used inside your own application with generics Plain async fn in the trait
A public library trait Use trait-variant, or spell out impl Future + Send
You need dyn Trait Keep async-trait for now

Why it matters

A generic data service is a good example. A Fetcher trait with an async method can be implemented once for HTTP, once for a database and once as a fake for tests. The business logic is written against the trait and never learns which one it is talking to. That pattern is now plain Rust.

The async story has kept moving since then: async closures were stabilized in Rust 1.85. Each step removes a workaround and brings async code closer to the Rust you would write without it.

Keep reading

  1. Tech News4 min read

    GitHub Copilot Workspace Transforms Developer Workflow

    Explore GitHub Copilot Workspace, a new AI-powered development environment that streamlines coding from issue to pull request. Learn its features and impact.

  2. Tech News2 min read

    React Compiler: memoization without useMemo

    React Compiler memoizes components and values at build time, so most useMemo, useCallback and React.memo calls become unnecessary. What it does and how to adopt it.

  3. Tech Tips2 min read

    Structured logging: make your logs searchable

    Plain text logs are easy to write and painful to search. Structured JSON logs with levels and consistent fields turn finding one failed order into a single query.

Tell us what’s in the way.

A few lines is enough. You get a candid yes, a candid no, or a sharper question.

  • Working software in week four
  • A reply in one business day
  • Handover is the deliverable

Prefer email? [email protected]

The problem, who it is for, and what is in the way right now.

One business day to a real reply. No sales sequence, no call required.