The Never Type ! and Infallible

Authors

K19G

grok-bot

Updated

October 31, 2026

The Never Type ! and Infallible

Thesis

Rust has always had a type for “this expression does not produce a value”: !, the never type. panic!(), loop {} without break, return, continue, and std::process::exit all have type !, and a function declared -> ! promises never to return. Until now you could only write ! in that return position. Everywhere else — Result<T, !>, type Error = !;, impl Trait for ! — it was an unstable feature, and the standard library used a stand-in, the empty enum std::convert::Infallible.

Rust 1.100 (scheduled for 2026-11-12) stabilizes ! as an ordinary type after a decade behind a feature gate, and makes Infallible a plain alias for it. The language change is small; the error-handling consequence is real: “this operation cannot fail” now has one canonical spelling, Result<T, !>, that the compiler understands well enough to let you skip the impossible Err arm entirely.

The same release changes never type fallback to ! on every edition, which is a small breaking change for code that quietly relied on the old fallback to (). This chapter shows both sides, compiled with the current beta and stable toolchains.

Mental model

            ┌──────────────────── every type ────────────────────┐
            │   u32   String   ()   Vec<T>   Result<T, E>   …   │
            └───────────────────────────▲─────────────────────────┘
                                        │  coerces to any type
                                        │  (unreachable code needs no value)
                                       `!`   — no values at all

Result<T, !>   =  Ok(T) | Err(<impossible>)      → only Ok can exist
size_of::<Result<u32, !>>() == size_of::<u32>()  → no discriminant needed
Infallible     =  !   (type alias since 1.100)

Three facts carry the whole chapter:

Fact Consequence
! has no values Code that “holds” one is unreachable; the compiler can delete branches that need it
! coerces to any type A diverging arm (die(…), panic!, return) fits in a match whose other arms are u32
An enum variant containing ! is uninhabited let Ok(v) = r; is irrefutable when r: Result<T, !>; the Err arm need not be written

Infallible was the library’s way to get the same effect (an enum with no variants). Since it is now literally !, existing APIs such as TryFrom implementations whose error is Infallible work unchanged and read the same way.

Worked examples

All listings were compiled and run on the box with rustc 1.100.0-beta.4 (2026-10-04) and, for comparison, rustc 1.99.0 stable, both installed with rustup. Until 1.100 reaches stable, run them with cargo +beta / rustc +beta:

rustup toolchain install beta --profile minimal
rustc +beta --version
rustc 1.100.0-beta.4 (a2eb6097d 2026-10-04)

Case 1: ! as a type — cannot-fail results, irrefutable Ok

cargo +beta new never-demo && cd never-demo
// src/main.rs — the never type `!` as a first-class type (Rust 1.100)
use std::convert::Infallible;
use std::process;

// Case 1: a diverging function. `-> !` has been legal here since Rust 1.0.
fn die(msg: &str) -> ! {
    eprintln!("fatal: {msg}");
    process::exit(2)
}

// Case 2: `!` written anywhere a type goes — new in 1.100.
type Never = !;

fn parse_port(s: &str) -> Result<u16, Never> {
    // This parser cannot fail: anything unparsable maps to the default port.
    Ok(s.parse().unwrap_or(8080))
}

// Case 3: a trait whose implementors pick an error type; `!` means "cannot fail".
trait Source {
    type Error: std::fmt::Debug;
    fn read(&mut self) -> Result<u32, Self::Error>;
}

struct Counter(u32);
impl Source for Counter {
    type Error = !; // reading a counter never fails
    fn read(&mut self) -> Result<u32, !> {
        self.0 += 1;
        Ok(self.0)
    }
}

fn main() {
    // `!` coerces to any type, so a diverging arm fits a u32 match.
    let args: Vec<String> = std::env::args().collect();
    let n: u32 = match args.get(1) {
        Some(s) => s.parse().unwrap_or_else(|_| die("not a number")),
        None => 3,
    };

    // Irrefutable: the Err arm of Result<_, !> is uninhabited, so no match needed.
    let Ok(port) = parse_port("9000");
    println!("port = {port}");

    let mut c = Counter(0);
    let mut total = 0;
    for _ in 0..n {
        let Ok(v) = c.read();
        total += v;
    }
    println!("sum of first {n} reads = {total}");

    // Infallible is now the same type as `!`.
    let same: fn(Infallible) -> ! = |x| x;
    let _ = same;
    let Ok(wide) = u64::try_from(7u8); // TryFrom via Into: Error = Infallible = !
    println!("wide = {wide}");
    println!("size_of::<Result<u32, !>>() = {}", std::mem::size_of::<Result<u32, !>>());
    println!("size_of::<Result<u32, String>>() = {}", std::mem::size_of::<Result<u32, String>>());
}
cargo +beta run -q
cargo +beta run -q -- 5
cargo +beta run -q -- five; echo "exit=$?"
port = 9000
sum of first 3 reads = 6
wide = 7
size_of::<Result<u32, !>>() = 4
size_of::<Result<u32, String>>() = 24
port = 9000
sum of first 5 reads = 15
wide = 7
size_of::<Result<u32, !>>() = 4
size_of::<Result<u32, String>>() = 24
fatal: not a number
exit=2

What to notice:

  • unwrap_or_else(|_| die("not a number")) type-checks because the closure returns !, which coerces to u32.
  • let Ok(port) = …; and let Ok(v) = c.read(); have no else and no match. The compiler knows Err(!) cannot be constructed. With Result<u16, String> the same line is a compile error (refutable pattern).
  • Counter::Error = ! documents “cannot fail” in the type, and callers that are generic over Source still handle Self::Error normally.
  • Result<u32, !> is the same size as a bare u32: no tag is stored for a variant that cannot exist.
  • fn(Infallible) -> ! accepting |x| x is the alias at work: the parameter is a !.

The same file on 1.99 stable stops at the first ! in type position:

cargo +stable build -q
error[E0658]: the `!` type is experimental
  --> src/main.rs:12:14
   |
12 | type Never = !;
   |              ^
   |
   = note: see issue #35121 <https://github.com/rust-lang/rust/issues/35121> for more information
...

(one such error per ! outside a return position). Until your minimum supported Rust version is 1.100, keep spelling it Infallible in public APIs; code that already uses Infallible keeps compiling and gains nothing by being rewritten.

Case 2: the fallback change — code that relied on () by accident

When the compiler meets a ! it must coerce, but nothing tells it the target type, it picks a fallback. That fallback used to be (); Rust 2024 switched it to !, and 1.100 does the same for editions 2015–2021. The most common casualty is ? on a function that is generic in its Ok type:

// fallback.rs — code that silently relied on `!` falling back to `()`
fn f<T: Default>() -> Result<T, ()> {
    Ok(T::default())
}

fn outer() -> Result<u32, ()> {
    f()?; // which T? nothing here says
    Ok(1)
}

fn main() {
    println!("{:?}", outer());
}

On 1.99, edition 2021: already rejected by a deny-by-default future-compatibility lint, which names the fix:

rustc +stable --edition 2021 fallback.rs
error: this function depends on never type fallback being `()`
 --> fallback.rs:6:1
  |
6 | fn outer() -> Result<u32, ()> {
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |
  = help: specify the types explicitly
note: in edition 2024, the requirement `!: Default` will fail
 --> fallback.rs:7:5
  |
7 |     f()?; // which T? nothing here says
  |     ^^^
  = warning: this was previously accepted by the compiler but is being phased out; it will become a hard error in Rust 2024 and in a future release in all editions!
  = note: for more information, see <https://doc.rust-lang.org/edition-guide/rust-2024/never-type-fallback.html>
  = note: `#[deny(dependency_on_unit_never_type_fallback)]` (part of `#[deny(rust_2024_compatibility)]`) on by default
help: use `()` annotations to avoid fallback changes
  |
7 |     f::<()>()?; // which T? nothing here says
  |      ++++++

error: aborting due to 1 previous error

On 1.100 beta, edition 2021 — the lint is gone because the behaviour changed; you get the real type error (identical to what 1.99 already says under edition 2024):

rustc +beta --edition 2021 fallback.rs
error[E0277]: the trait bound `!: Default` is not satisfied
 --> fallback.rs:7:5
  |
7 |     f()?; // which T? nothing here says
  |     ^^^ the trait `Default` is not implemented for `!`
  |
  = note: this error might have been caused by changes to Rust's type-inference algorithm (see issue #148922 <https://github.com/rust-lang/rust/issues/148922> for more information)
  = help: you might have intended to use the type `()` here instead
note: required by a bound in `f`
 --> fallback.rs:2:9
  |
2 | fn f<T: Default>() -> Result<T, ()> {
  |         ^^^^^^^ required by this bound in `f`

The fix is to say what you meant:

// fallback_fixed.rs — name the type instead of relying on fallback
fn f<T: Default>() -> Result<T, ()> {
    Ok(T::default())
}

fn outer() -> Result<u32, ()> {
    f::<()>()?; // say it: the Ok value is ()
    Ok(1)
}

fn main() {
    println!("{:?}", outer());
}
rustc +beta --edition 2021 fallback_fixed.rs && ./fallback_fixed
Ok(1)

Case 3: the panicking closure

The second common pattern is a closure that only panics, passed to something that requires its return type to implement a trait:

// closure.rs — a panicking closure used to "return ()" by accident
trait Unit {}
impl Unit for () {}

fn run<R: Unit>(f: impl FnOnce() -> R) {
    let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f));
}

fn main() {
    run(|| panic!("boom"));
    println!("survived");
}
rustc +beta --edition 2024 closure.rs
error[E0277]: the trait bound `!: Unit` is not satisfied
  --> closure.rs:10:5
   |
10 |     run(|| panic!("boom"));
   |     ^^^^^^^^^^^^^^^^^^^^^^ the trait `Unit` is not implemented for `!`
   |
help: the trait `Unit` is implemented for `()`
  --> closure.rs:3:1
   |
 3 | impl Unit for () {}
   | ^^^^^^^^^^^^^^^^
   = note: this error might have been caused by changes to Rust's type-inference algorithm (see issue #148922 <https://github.com/rust-lang/rust/issues/148922> for more information)

Annotate the closure’s return type: run(|| -> () { panic!("boom") });

rustc +beta --edition 2024 closure_fixed.rs && ./closure_fixed
thread 'main' (250345) panicked at closure_fixed.rs:10:20:
boom
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
survived

Case 4: Infallible and ! are one type now

If a crate implemented a trait for both — typically to support nightly users of ! — the two impls now overlap:

// conflict.rs — Infallible and ! are now one type
use std::convert::Infallible;
trait Describe { fn describe() -> &'static str; }
impl Describe for Infallible { fn describe() -> &'static str { "infallible" } }
impl Describe for ! { fn describe() -> &'static str { "never" } }
fn main() { println!("{}", <! as Describe>::describe()); }
rustc +beta --edition 2024 conflict.rs
error[E0119]: conflicting implementations of trait `Describe` for type `!`
 --> conflict.rs:5:1
  |
4 | impl Describe for Infallible { fn describe() -> &'static str { "infallible" } }
  | ---------------------------- first implementation here
5 | impl Describe for ! { fn describe() -> &'static str { "never" } }
  | ^^^^^^^^^^^^^^^^^^^ conflicting implementation for `!`

Delete one of them. Keep the Infallible impl if you still support compilers older than 1.100: it covers ! on new compilers for free.

Where ! belongs in error handling

Situation Spell it Why
Function that never returns (exit, exec, server loop) fn run() -> ! Callers can use it in any expression position
Trait with an associated error type; this impl cannot fail type Error = !; Callers generic over the trait still compile; concrete callers write let Ok(v) = …;
Public API that must build on Rust < 1.100 Infallible Same type on new compilers, still compiles on old ones
“Can’t fail yet” — you expect failures later A real error type Changing ! to MyError later is a breaking API change
Converting between error types with ? impl From<Infallible> for MyError if needed Works for ! too, since it is the same type

The trap

Relying on inference to pick () for you. Code like f()?;, run(|| panic!()), or if c { Default::default() } else { return } compiled for years because the never type quietly became () when nothing else constrained it. Under 1.100 (any edition) or edition 2024 that guess is !, and the code either fails to compile — the good case, shown above — or, in the rare unsafe case the never_type_fallback_flowing_into_unsafe lint exists for, picks a different type than the author assumed. The compiler’s 1.99 lint (dependency_on_unit_never_type_fallback) points at every affected function: fix those before you bump the toolchain, not after.

The second, quieter trap is using ! as a placeholder in a public trait impl or function signature because “it can’t fail today”. ! is a promise. Replacing it with a real error type later breaks every caller who wrote let Ok(v) = …;.

The boring rule

Use -> ! for functions that truly never return and Result<T, !> (or Infallible until your MSRV is 1.100) only where failure is impossible by construction. Never lean on fallback: when the compiler cannot see a type, write it — f::<()>()?, || -> () { … }, () = expr;. Turn on the rust_2024_compatibility lints on older editions and clear them before upgrading.

Try this

  1. Change parse_port to return Result<u16, String>. Which lines stop compiling, and what does the compiler suggest for let Ok(port) = …;?
  2. Print size_of::<Option<!>>() and size_of::<Result<!, String>>() and explain the numbers.
  3. Write fn absurd<T>(x: !) -> T { x } and use it to implement fn into_ok<T>(r: Result<T, !>) -> T with a match that has a single arm.
  4. Take a crate you maintain on edition 2021, run cargo +stable check (1.99 today) and look for dependency_on_unit_never_type_fallback; then cargo +beta check and confirm every site is fixed.
  5. Find a TryFrom impl in std whose Error is Infallible and destructure its result with an irrefutable let.

Sources