Deep Dive into Async Traits in Rust: Why They Still Aren't Object-Safe and Practical Workarounds
Encountering error E0038 when using async in Rust traits? This article explores the structural causes and 3 practical workarounds.

Stock photo for illustration only, not from the actual event
- Async functions in Rust traits cannot achieve object safety, resulting in error E0038 when stored in a Vec by Somchai.
- The primary cause stems from the structural translation of async fn into State Machines with varying sizes for each implementor, which violates vtable requirements.
- The compiler development team has confirmed that this issue will not be resolved anytime soon, requiring developers to rely on alternative approaches.
- There are 3 main patterns for workarounds, depending on the suitability of your codebase and the library you are developing.
Many Rust developers may have experienced the frustrating situation of writing a trait with asynchronous methods, applying it to various data types, and attempting to bundle them together into a dynamic dispatch collection like Vec<Box<dyn Notifier>>. What appears on the compiler screen is the error message error[E0038]: the trait Notifier cannot be made into an object, which comes as quite a surprise because a similar-looking code pattern works seamlessly with synchronous traits. However, the moment a single async keyword is added to a method, object safety completely breaks down.
The underlying cause of this limitation is not the lack of any single feature, but rather a deeply embedded structural issue. The compiler translates async fn into a State Machine compatible with the Future trait, where the concrete type of these State Machines is anonymous and has varying memory sizes depending on each developer implementing it. Meanwhile, the vtable system of dynamic dispatch relies on method slot sizes that are strictly fixed and uniform across all implementors. These two architectural facts cannot be reconciled, and the Rust compiler development team has previously clarified that there are no plans to adjust or resolve this issue in the near future.
From a compiler architecture perspective, performing Dynamic Dispatch in a statically typed language like Rust requires a vtable (Virtual Method Table) to point to the correct function location at runtime, which in turn requires the exact size and data type to be known in advance. However, Futures from asynchronous functions have variable sizes depending on internal states and captured variables within that code block. The conflict between the flexibility of Futures and the rigidity of vtables remains a difficult challenge faced by many modern programming languages.
When architectural limitations prevent normal usage, the crucial question is how developers should handle this problem. The currently effective workarounds are divided into 3 main patterns, where choosing the most appropriate approach depends on the context of the dataset—such as whether users form a closed set entirely under your control, an open set from third-party external code, or whether you are developing a library where the overhead of using Box could impact end-users.

Stock photo illustration, not an actual photo from the event
Source: Dev.to
Found something wrong in this article? Report an issue with this article
Comments
Leave a Comment