What Is the Deref Trait and How Does It Enable Smart Pointer Ergonomics?

The Deref trait allows smart pointers to be treated like regular references by enabling automatic dereferencing for ergonomic syntax.

The star operator is a leak

You build a custom wrapper to manage a resource. Inside, you store a String. You want to print its length. You write wrapper.len(). The compiler rejects you. You write (*wrapper).len(). It compiles. You add another layer of indirection. Now you need (*(*wrapper)).len(). The stars pile up. The syntax turns into a hazard.

The Deref trait stops the cascade. It lets the compiler insert the dereference operators for you, so your wrapper behaves exactly like the value it holds. You write wrapper.len() and the compiler quietly translates it to (*wrapper).len(). The ergonomics match standard references. The code stays readable.

The remote control contract

Think of a smart pointer as a remote control for a device. The device is your actual data. The remote holds a reference to the device and maybe some extra logic, like a battery indicator or a sleep timer. When you press the "Volume Up" button on the remote, you expect the volume to go up. You don't expect the remote to say, "I don't have a volume button, you need to walk over to the TV and press it there."

Deref is the wiring inside the remote that maps every button press to the corresponding action on the device. It makes the remote transparent. You interact with the remote, but the effect happens on the device. The trait defines what type lives inside the wrapper and provides a reference to it. The compiler uses that reference to resolve method calls and match types automatically.

Minimal example

The trait requires one associated type and one method. The associated type Target tells the compiler what you point to. The method deref returns a reference to that target.

use std::ops::Deref;

/// A wrapper that owns a value of type T.
struct MyBox<T>(T);

impl<T> Deref for MyBox<T> {
    // The type we point to.
    type Target = T;

    // Return a reference to the inner value.
    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

fn main() {
    let s = MyBox(String::from("hello"));

    // Without Deref, you'd need (*s).len().
    // With Deref, the compiler inserts the star for you.
    // This calls String::len() directly.
    println!("Length: {}", s.len());
}

The call s.len() works because MyBox implements Deref. The compiler sees that MyBox doesn't have a len method. It dereferences s to get a String. It finds len on String and calls it. The syntax is clean. The wrapper is invisible.

Coercion happens in two places

Deref coercion operates in two distinct modes. Method resolution handles function calls. Type coercion handles argument matching. Both use the same underlying mechanism, but they trigger differently.

Method resolution kicks in when you call a method on a type that implements Deref. The compiler searches for the method on the current type. If it doesn't find it, the compiler dereferences the value and searches again. This repeats until the method is found or the chain ends. You can chain multiple derefs. If A derefs to B and B derefs to C, calling a method on C works through A.

Type coercion happens when you pass a reference to a function that expects a different reference type. If a function takes &String, you can pass &MyBox<String>. The compiler coerces &MyBox<String> to &String automatically. This works for &T to &U where T derefs to U. It also works for &mut T to &mut U if T implements DerefMut.

Convention aside: Deref is for read-only access. If your wrapper needs to mutate the inner value, implement DerefMut as well. The community expects both if the inner value is mutable. Implementing Deref without DerefMut signals that the wrapper protects the data from mutation.

Realistic example: a full smart pointer

Real smart pointers implement both Deref and DerefMut. This gives full ergonomic access. The wrapper can expose the inner value for reading and writing without forcing the user to manage stars.

use std::ops::{Deref, DerefMut};

/// A heap-allocated wrapper similar to Box.
struct MyBox<T>(Box<T>);

impl<T> Deref for MyBox<T> {
    type Target = T;

    fn deref(&self) -> &T {
        &self.0
    }
}

impl<T> DerefMut for MyBox<T> {
    fn deref_mut(&mut self) -> &mut T {
        &mut self.0
    }
}

fn main() {
    let mut x = MyBox(String::from("hello"));

    // Deref coercion for method call.
    // Calls String::push_str via DerefMut.
    x.push_str(" world");

    // Deref coercion for type matching.
    // &x is &MyBox<String>, coerced to &str.
    let s: &str = &x;
    println!("{}", s);
}

The DerefMut trait mirrors Deref but takes &mut self and returns &mut T. It enables mutable method calls and mutable type coercion. Note that DerefMut requires Deref. The trait definition ties the target type to Deref::Target. You cannot implement DerefMut without Deref. The compiler enforces this dependency.

The ambiguity rule

Deref is a fallback, not a replacement. If your wrapper defines a method with the same name as the inner type, the wrapper's method wins. The compiler never derefs to find a method if the current type has one. This prevents shadowing surprises.

use std::ops::Deref;

struct Wrapper(String);

impl Deref for Wrapper {
    type Target = String;
    fn deref(&self) -> &String {
        &self.0
    }
}

impl Wrapper {
    // This shadows String::len.
    fn len(&self) -> usize {
        999
    }
}

fn main() {
    let w = Wrapper(String::from("hi"));
    // Calls Wrapper::len, not String::len.
    // Result is 999, not 2.
    println!("{}", w.len());
}

The output is 999. The compiler finds len on Wrapper and stops searching. It never looks at String. This rule keeps method resolution predictable. You can override inner methods safely. The wrapper always speaks first.

If you call a method that only exists on the inner type and Deref is missing, the compiler rejects you with E0599 (no method named ... found). The error points to the wrapper type and lists the available methods. It won't suggest dereferencing unless Deref is implemented.

Pitfalls and errors

Side effects in deref break the contract. The community expects deref to be a pure pointer projection. It should return a reference instantly. It should not log, count, allocate, or panic. If deref has side effects, you break the mental model. Readers assume deref is cheap and transparent. Code that relies on that assumption will misbehave.

If you need to track access, use a method. Don't hide logic inside deref. The compiler may call deref in ways you don't expect. Method resolution can trigger multiple derefs. Type coercion can trigger derefs during argument passing. If deref prints to console, your code might print twice for a single logical access. The behavior becomes unpredictable.

Treat deref as a zero-cost projection. If it does work, you're lying to the compiler and the reader.

Another pitfall is infinite recursion. If Deref returns a type that derefs back to the original, the compiler detects the cycle and rejects the code. You'll get an error about recursive type definitions or infinite expansion. This is rare in practice because the types usually differ, but it's a constraint to keep in mind.

Convention aside: Deref is for smart pointers. If your type is a Config wrapper or a Database client, don't impl Deref for the inner type. Impl specific methods. Deref implies the wrapper is transparent. If it's not transparent, don't impl Deref. Using Deref on opaque wrappers exposes implementation details and couples the API to the inner type. Reach for explicit methods when the wrapper adds distinct behavior.

Decision matrix

Use Deref when you want a wrapper to expose the methods of the inner type without forcing the user to type stars. Use Deref for read-only access to the wrapped value. Use DerefMut when the wrapper needs to allow mutation of the inner value through the same ergonomic syntax. Use direct field access when the wrapper adds distinct behavior that shouldn't be confused with the inner type's methods. Reach for Deref on every smart pointer you build; it's the standard contract for "this type holds a value and lets you use it."

Where to go next