Mehdi Akiki
Rust Failure Atlas / Language and diagnostics

RFA-540 · Case file with fixtures · Case 512 of 694 · Compiler evidence

Rust Inherent Methods Cannot Be Added Directly to Primitive Types

Inherent method namespaces belong to the defining type. Extension traits add opt-in syntax, while newtypes create a local type with stronger domain identity.

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
Inherent method namespaces belong to the defining type and cannot accept competing additions from arbitrary downstream crates.
First discriminating check
Choose a local extension trait for opt-in syntax, a newtype for domain identity and invariants, or an ordinary free function.

I wanted 20_u64.as_retry_budget() and first tried to place the method directly in impl u64. E0390 explains why this apparently local convenience would not remain local: inherent method names are controlled by the crate defining the type.

The failing fixture adds an inherent method to u64. A primitive is not a nominal type defined by this crate, so Rust rejects the impl.

Inherent methods share one type namespace

An inherent implementation has no trait name:

impl MyType {
    fn operation(&self) {}
}

These methods are found directly from the type. If every dependency could add inherent methods to u64, two crates could choose the same name with different meaning. Bringing them together would make ordinary method calls ambiguous or dependent on unrelated dependencies.

Rust gives the defining crate authority over that namespace. The Reference states the locality rules for inherent implementations, and the official E0390 page recommends a trait as one repair.

An extension trait makes the method opt in

The repaired fixture defines RetryBudgetExt locally and implements it for u64. The type is foreign, but the trait belongs to this crate, so the coherence rules permit the implementation.

Callers must have the trait in scope for method syntax. This avoids claiming the global inherent namespace. Two libraries may provide different extension traits with same-named methods, and a caller can choose which contract to import or use fully qualified syntax when needed.

I give extension traits domain-specific names and keep their method sets small. A generic U64Ext with unrelated helpers quickly becomes a hidden utility drawer.

A newtype is stronger when the integer has meaning

Retry budgets are not merely any u64. A value may have a maximum, unit, validation rule, or different arithmetic from byte counts and timestamps. In production code I would often prefer:

struct RetryBudget(u64);

Because RetryBudget is local, it may have inherent constructors and methods. It also prevents accidentally passing a byte count where a retry budget is expected.

The Rust Book describes the newtype pattern in the context of trait coherence. The same wrapper gives us a controlled inherent namespace.

The two repairs serve different API goals

An extension trait is useful when every existing value of the external type can meaningfully perform the operation and wrapping would be unnecessary ceremony. Formatting helpers and generic slice conveniences often fit.

A newtype is useful when construction establishes invariants or when the value should not mix with other values of the same representation. Money, identifiers, durations, limits, and state tokens usually benefit from domain identity.

I do not choose solely by which version uses fewer lines. I ask whether the operation extends a representation or belongs to a new concept.

Free functions remain a clear option

retry_budget(20) may be simpler than either abstraction, especially inside one module. Functions are easy to search, do not require trait imports, and can make conversions visible.

Method syntax is valuable when it improves a fluent, discoverable interface. It is not a requirement for good Rust. E0390 can be an invitation to keep a helper ordinary rather than inventing a public trait.

References and raw pointers are not local nominal types either

The error documentation also shows inherent impl attempts on pointer-like primitive forms. Even when a reference points to my local Foo, &Foo is not itself the locally defined nominal type. I place borrowing receivers inside impl Foo:

impl Foo {
    fn compare(&self, other: &Self) {}
}

This preserves ownership of the method namespace while still accepting references at the call boundary.

My E0390 checklist

  • Is this an inherent impl with no trait name?
  • Was the target type defined in this crate?
  • Does the operation apply naturally to all values of the external type?
  • Would a local extension trait provide clear opt-in syntax?
  • Does the value need a newtype for invariants or unit safety?
  • Would a free function be easier to understand and import?
  • Can the method live on my local pointee type rather than a reference type?
  • Is the public name specific enough to avoid extension-trait collisions?

The core principle is that inherent method namespaces have an owner. I use extension traits for optional syntax, newtypes for domain identity, and free functions when no extra abstraction is needed.