Introduction
Marker traits have no methods — they mark types with compile-time properties. Rust's auto traits (Send, Sync, Copy, Sized, Unpin) are marker traits that the compiler implements automatically. Since Rust 1.85, the new AsyncFn, AsyncFnMut, and AsyncFnOnce traits join the prelude for async closure support.
Key Concepts
- Marker Trait: A trait with no methods that marks a type with a property.
- Auto Trait: A marker trait automatically implemented by the compiler if all fields satisfy it.
- Negative Impl: Explicitly opting out of an auto trait:
impl !Send for MyType {}. - Sealed Trait: A pattern that prevents external crates from implementing your trait.
Real World Context
Send and Sync are the foundation of Rust's thread safety. The compiler checks these markers to prevent data races at compile time. Copy marks types that can be duplicated by simple bit copying. Custom marker traits are used to create type-level permissions and capabilities.
Deep Dive
Compiler-Provided Markers
rusttrait Send {} // Safe to transfer between threads trait Sync {} // Safe to share references between threads trait Copy {} // Bitwise copyable (`Copy` itself has no methods, but it requires `Clone` which does) trait Sized {} // Known size at compile time trait Unpin {} // Safe to move after pinning
Sized has special compiler treatment: it is implicitly assumed for all type parameters unless opted out with ?Sized. This is unlike other auto traits which are simply inferred from field types.
Auto traits are implemented automatically if all fields satisfy them:
ruststruct MySendType { x: i32, // Send ✓ s: String, // Send ✓ } // MySendType: Send ✓ (auto-implemented)
Custom Marker Traits
rusttrait JsonSerializable {} impl JsonSerializable for i32 {} impl JsonSerializable for String {} impl<T: JsonSerializable> JsonSerializable for Vec<T> {} fn to_json<T: JsonSerializable>(value: &T) -> String { todo!() // Can only be called with marked types }
Sealed Traits
Prevent external crates from implementing your trait:
rustmod private { pub trait Sealed {} } pub trait MyTrait: private::Sealed { fn method(&self); } // Only types you mark as Sealed can implement MyTrait impl private::Sealed for MyType {} impl MyTrait for MyType { fn method(&self) { /* ... */ } }
AsyncFn Traits (Rust 1.85+)
While not marker traits themselves (they have methods), several important traits were added to the Rust 1.85 prelude that are worth mentioning alongside marker traits.
Rust 1.85 added AsyncFn, AsyncFnMut, and AsyncFnOnce to the prelude. These are the async counterparts of Fn, FnMut, and FnOnce:
rustasync fn call_async(f: impl AsyncFn(i32) -> String) -> String { f(42).await }
Note that AsyncFn* traits are NOT dyn-compatible.
Common Pitfalls
- Assuming all traits have methods — Marker traits intentionally have no methods. Their value is in the type-level contract they express.
- Negative impls in stable Rust —
impl !Send for T {}is only available on nightly. On stable, usePhantomData<*const ()>to opt out of bothSendandSync(raw pointers are neither).
Best Practices
- Use sealed traits for public API stability — Sealing prevents external breakage when you add methods to the trait.
- Prefer auto-derived markers — Let the compiler derive
SendandSyncautomatically; only add manual impls when wrapping raw pointers or FFI types.
Summary
- Marker traits have no methods and mark types with compile-time properties.
- Auto traits (
Send,Sync,Copy,Sized,Unpin) are implemented automatically by the compiler. - Sealed traits prevent external implementations using a private supertrait.
AsyncFn,AsyncFnMut,AsyncFnOnce(Rust 1.85) are the async closure traits.
Code Examples
// Custom marker trait for type-level permissions
trait CanRead {}
trait CanWrite {}
struct File<R, W> {
path: String,
_read: std::marker::PhantomData<R>,
_write: std::marker::PhantomData<W>,
}
struct Yes;
struct No;
impl CanRead for Yes {}
impl CanWrite for Yes {}
impl<W> File<Yes, W> {
fn read(&self) -> Vec<u8> { todo!() } // Only if R = Yes
}
impl<R> File<R, Yes> {
fn write(&self, _data: &[u8]) { todo!() } // Only if W = Yes
}
// Compile-time enforced: read-only file cannot write
let ro_file: File<Yes, No> = todo!();
ro_file.read(); // ✓
// ro_file.write(b"data"); // ✗ Compile error!