Mehdi Akiki
Rust Failure Atlas / Language and diagnostics

RFA-530 · Case file with fixtures · Case 502 of 694 · Compiler evidence

Rust Method Receivers Must Lead Back to Self

The first self parameter is not ordinary dependency injection. Its type must identify the type whose method namespace contains the function.

Reviewed
Rust
Rust 1.98.1, edition 2024
Targets
all Rust targets
Profiles
dev, release, test

Direct answer

What this Rust failure means

Why it happens
The self parameter carries method ownership and lookup identity, so it cannot be replaced by an unrelated dependency type.
First discriminating check
Identify the type owning the impl, choose a supported receiver rooted in Self, and pass unrelated collaborators as ordinary parameters.

I once read self: SomeType as if self were only a conveniently named first argument. E0307 is the useful correction: a method receiver has a structural relationship with the type owning the method.

The failing fixture opens impl Cache but declares fn refresh(self: &Scheduler). Scheduler may be a perfectly sensible dependency. It is not the receiver of a method stored in the Cache namespace.

A receiver answers which value owns this call

The familiar forms are self, &self, and &mut self. They expand conceptually to self: Self, self: &Self, and self: &mut Self. Inside impl Cache, Self means Cache.

That connection enables cache.refresh(). Method-call lookup begins from the type of cache, follows the permitted receiver adjustments, and finds associated methods whose receiver can be reached. A completely unrelated &Scheduler breaks this model.

The official E0307 explanation lists the stable receiver families and explains the role of Self.

The repaired method borrows the actual owner

The repaired fixture gives Cache a generation field and uses fn refresh(&self). The call now reads state from the receiver that selected the method.

If refreshing also needs a scheduler, I pass it as a second parameter:

fn refresh(&self, scheduler: &Scheduler) -> u64

This signature tells the truth. The cache owns the operation; the scheduler collaborates with it. Swapping those roles merely to satisfy syntax would produce a confusing API.

Box, Rc, Arc, and Pin receivers still preserve identity

Rust accepts some explicit forms such as self: Box<Self>, self: Rc<Self>, self: Arc<Self>, and supported Pin wrappers. These forms change how the receiver is owned or accessed, but the chain still leads to Self.

For example, a consuming operation may use self: Box<Self> when the value is already heap-owned. An async or self-referential abstraction may expose a pinned receiver. Those are ownership choices around the same underlying method owner, not arbitrary first-argument types.

The Reference describes receivers in its method section. I check that grammar before designing exotic receiver syntax.

Deref affects method lookup but is not invisible inheritance

Method calls may use dereference adjustments. That is why wrapper types can often call methods of their target. The Deref documentation also warns that this has API-design consequences because methods become part of lookup.

I do not implement Deref only to force a desired method receiver. I use it when transparent pointer-like behaviour is genuinely the abstraction. A service container or scheduler is usually composition, so an ordinary parameter or field is clearer.

Arbitrary self types extend receiver possibilities on nightly, but this does not turn self into an unrestricted argument. The receiver still has to follow an accepted chain toward Self. I keep stable public APIs on stable rules unless the project deliberately owns a nightly toolchain.

An associated function is sometimes the honest design

If an operation does not need a Cache value, I omit self entirely:

impl Cache {
    fn from_scheduler(scheduler: &Scheduler) -> Cache {
        // construct a cache
    }
}

This is an associated function and callers use Cache::from_scheduler(...). The path says that Cache organises the constructor, while the scheduler is ordinary input. Trying to disguise this as scheduler.from_cache_namespace() would blur ownership.

There is another easy mistake: a trait declares fn refresh(&self), while its implementation declares a different valid receiver form or parameter type. That may produce a trait compatibility diagnostic such as E0053. E0307 is more basic: the written receiver type itself is not an accepted receiver for the surrounding Self.

I preserve this distinction when searching compiler errors. Similar-looking source can fail during different checks, and the repair should follow the check that actually rejected it.

My receiver checklist

  • Which type's impl block contains the method?
  • Does the receiver resolve to that Self through a supported form?
  • Should the operation borrow, mutably borrow, or consume its owner?
  • Is an unrelated service really a second argument or stored dependency?
  • Does the operation need a receiver at all?
  • Am I using Deref because the wrapper is pointer-like, or only for syntax?
  • Is a nightly receiver feature an intentional compatibility cost?
  • Does the call syntax communicate who owns the behaviour?

The core principle is that self carries method identity, not only data. Once I decide which value owns the operation, the correct receiver and the place for collaborators become much easier to see.