December 11, 2025·5 min read·engineeringrusttypesinferencecompatibility

Rust 1.92 makes never-type fallback dependencies errors

Two deny-by-default lints expose code whose inferred types depend on historical unit fallback. The change prepares existing Rust code for different never-type semantics without changing every edition at once.

Ignoring a function's result does not mean its result type is irrelevant. The compiler may still need that type to select a trait implementation or determine what an unsafe function creates. Rust's historical handling of diverging expressions could fill an inference gap with (), making the choice easy to overlook. Changing that fallback can therefore break code that appears to have no meaningful result at all.

Rust 1.92, released on December 11, makes two lints deny by default to expose these dependencies. They are dependency_on_unit_never_type_fallback and never_type_fallback_flowing_into_unsafe. Both concern the point where the compiler has insufficient information and must choose a fallback type. The release makes affected code fail compilation when built directly, while retaining the ability to allow the lints explicitly.

I find this part of Rust more compelling than syntax changes. Its usefulness as an alternative to C depends partly on making assumptions visible at unsafe boundaries. A type selected because inference ran out of information deserves particular attention when that type controls whether an operation is sound.

Unit and an expression that never produces a value

The unit type, (), has a value, also written (). A function returning unit can finish normally. The never type, !, has no values. A diverging expression, such as a panic or a return from the enclosing function, does not produce a value for its surrounding expression. Those facts are different even when the surrounding code discards its result.

The edition guide's explanation of never-type fallback describes how divergence participates in inference. At a coercion site, a value of never type can coerce to another type. The guide presents this conceptually as an inserted conversion function from ! to a generic T. There is no value for that function to convert at runtime, but its output type still participates in type checking.

Usually, context determines that output type. When it cannot, the compiler needs a fallback. Historically, that fallback was unit. Rust 2024 changes it to never type. Earlier editions retain their historical fallback, so the edition distinction matters when interpreting the new diagnostics. Rust 1.92's lint changes do not mean every crate suddenly uses Rust 2024 inference rules.

The release also does not announce complete stabilization of the never type. Its announcement explicitly describes that work as ongoing. These lints address existing code that depends on an inference behavior which obstructs that work. Treating them as a general announcement that every use of ! is now stable would misread the change.

A question mark can leave the success type unresolved

The dependency is not limited to code that visibly contains a panic. The edition guide gives a generic function whose call uses ?. This reduced example preserves that shape while stating the intended success type explicitly.

fn make_value<T: Default>() -> Result<T, ()> {
    Ok(T::default())
}

fn run() -> Result<(), ()> {
    make_value::<()>()?;
    Ok(())
}

Without ::<()>, the statement does not otherwise specify which T the function should return. The question-mark operator's desugaring introduces divergence on the error path. The historical fallback can cause the unconstrained success type to become unit. Under the changed fallback, inference can instead select never type, which does not satisfy the required Default bound in the guide's example.

The annotation repairs the actual ambiguity. It says that this call should produce unit on success, regardless of fallback. Adding an allow attribute would leave that choice implicit. There is no universal automatic fix because the compiler cannot know whether unit was intended or merely happened to make the program compile.

Another example in the guide passes a panicking closure to a generic function whose output must implement a trait. If only unit implements that trait, historical fallback can select that implementation. An explicit closure return type, || -> () { panic!() }, records the intended result. The closure still diverges, but its declared output no longer depends on the fallback rule.

These examples explain why apparently unused results can affect compilation. Trait selection needs a concrete type even when the program never inspects the corresponding value. A discarded result is not an instruction to erase its type obligations.

Unsafe inference can change the validity of an operation

The unsafe lint covers a more serious consequence. In the compiler's Rust 1.92 lint definitions, an example uses an if expression with a diverging branch and a call to std::mem::zeroed() in the other branch. The call's generic return type is initially unspecified. Historical fallback can select (). A different fallback can select !.

Creating a unit value with that operation is sound in the example. Attempting to create a value of never type is undefined behavior because no such value exists. The relevant distinction concerns the validity of the constructed value, even though the enclosing statement discards its result. Merely ignoring a value cannot make an invalid construction sound.

The lint documentation repairs its example by specifying std::mem::zeroed::<()>(). That is an explanation of the particular inference hazard, not a reason to use unsafe construction for an ordinary unit value. Existing unsafe code needs to state the type its safety argument assumes. A fallback selected elsewhere in an expression should not supply that part of the argument silently.

I prefer explicit types at this boundary. Rust's inference is valuable when it removes repetition, but a safety argument becomes harder to inspect when a generic unsafe call depends on historical fallback. The new default makes that dependency a compiler error before a later semantic change can expose it differently.

A staged compatibility change

Deny by default describes a lint level. The release announcement says an affected crate built directly will fail compilation unless the lint is allowed. When built as a dependency, the lint does not itself prevent compilation, and Cargo reports the future-compatibility problem. That distinction lets the ecosystem discover affected dependencies without making every downstream build fail immediately.

Allowing a lint can provide a temporary escape, but it does not specify the missing type or resolve the dependency. An explicit annotation addresses the reason for the diagnostic. The two lints also identify different consequences. One detects reliance on unit fallback for code to compile, while the other identifies fallback flowing into unsafe calls.

Rust 1.92 contains a related change in the opposite diagnostic direction. Its unused_must_use lint no longer warns for a Result<(), UninhabitedType> or a ControlFlow<UninhabitedType, ()>. The release gives Result<(), Infallible> as an example. An error type with no possible values makes the error branch impossible, while the successful value is unit. Discarding that result therefore has a different meaning from discarding an ordinary fallible result.

That adjustment depends on information already present in the type. The never-type fallback errors concern information the source left unspecified. Together, the changes make diagnostics follow the distinction between a known impossible outcome and an inference gap that historical behavior happened to fill.