Mehdi Akiki
Rust Failure Atlas / Upgrades and compatibility

RFA-521 · Case file with fixtures · Case 493 of 694 · Compiler evidence

Rust Negative Trait Impls Cannot Be Marked Unsafe

An unsafe positive impl promises safety invariants; a negative impl denies a capability and therefore does not make that promise. Negative impl support also remains restricted and unstable in general use.

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
Unsafe positive implementations promise invariants that safe callers rely on, while denying a capability grants no such operation and has separate stability restrictions.
First discriminating check
Remove the invalid safety marker and verify whether stable field composition, a compile-fail assertion, or an explicitly pinned nightly feature expresses the real auto-trait intent.

I wrote unsafe impl !Send for Local. Rust emitted E0198 because a negative implementation denies a trait relationship and cannot be marked unsafe.

The failing fixture also encounters the broader stability restrictions around negative impls. The case records E0198 as the direct structural error while keeping the stable repair free of experimental features.

Unsafe impl makes a positive promise

For an unsafe trait, unsafe impl Trait for Type asserts that the implementor upholds invariants on which other safe code may rely. Getting that assertion wrong can make safe use unsound.

A negative impl says the relationship does not exist. It grants no capability to generic callers and therefore is not an unsafe positive promise.

The official E0198 explanation says negative implementations are always safe in this sense.

The repaired fixture avoids an unsupported assertion

The repaired fixture defines Local without a manual negative impl. This is the stable structural repair for the minimal example.

It does not claim that Local is non-Send; an empty struct will normally acquire auto traits. If non-thread-transferability is a real invariant, the type's fields should enforce it on stable Rust.

I do not confuse successful compilation with preserved auto-trait semantics.

Field composition can opt out of auto traits

Auto traits such as Send and Sync are derived from the type's contents. A local type containing a suitable non-Send marker or resource will itself not be Send.

The field should correspond to the real reason movement is unsafe—for example, thread-affine state—not exist as mysterious compiler bait. I document why the type must remain on one thread.

The Rustonomicon chapter on Send and Sync explains these unsafe auto traits.

Negative impl stability is a separate question

Removing unsafe addresses E0198, but negative impl syntax may still require unstable support depending on the trait and toolchain. One diagnostic can therefore reveal two independent issues: wrong safety marking and unstable language surface.

I verify stable Rust rather than assuming the official example's feature-gated form belongs in a production crate. For compiler experiments, I pin nightly and record the feature explicitly.

Each problem deserves its own evidence.

Unsafe positive auto-trait impls need strong proof

If a type does not automatically implement Send or Sync, manually asserting it with unsafe impl transfers responsibility to me. I must prove that internal pointers, aliasing, and thread-affinity rules make movement or shared access safe.

This is the opposite direction from a negative impl. I never add a positive unsafe impl merely to satisfy a spawn or shared-state error.

The review should begin from the contained fields and their invariants.

API design should make thread affinity visible

A thread-bound handle can expose operations through an executor or owner that lives on the required thread. This is often clearer than relying on users to discover non-Send behaviour deep in an async spawn error.

Constructors, documentation, and tests can show the intended execution context. Auto-trait behaviour is part of a public type's compatibility surface even though it is not a named method.

Compile-time assertions protect auto-trait intent

For types expected to be Send, a small helper such as fn assert_send<T: Send>() {} can verify the positive contract. Stable Rust has fewer direct ways to assert that a type is not Send, so I use compile-fail tests or a dedicated compile-testing harness when the negative property matters.

This prevents a field refactor from silently changing thread behaviour. Replacing a thread-affine field with an integer handle, for example, may cause automatic Send derivation even though the external resource remains thread-bound. Representation no longer enforces the domain invariant, so the wrapper design must restore it deliberately. I review auto traits whenever raw handles, markers, or ownership containers change.

My E0198 checklist

  • Is the impl positive or negative?
  • Does unsafe represent a real positive trait invariant?
  • Is negative impl syntax stable for this use and toolchain?
  • Must the type truly remain non-Send or non-Sync?
  • Which field or resource creates that thread-affinity reason?
  • Would composition enforce the invariant on stable Rust?
  • Am I tempted to add an unsafe positive impl only to satisfy a bound?
  • Are the auto-trait semantics tested at the API boundary?

The core principle is that unsafe marks a capability promise requiring proof. Denying a capability is not that promise, and on stable Rust the type's composition should express real auto-trait constraints where possible.