The Never Type
panic!, return, break, std::process::exit, loop {}, these expressions never produce a value, because control flow never continues past them. Rust has a type for exactly that: !, the never type (also called the "empty type" or "bottom type"). It's easy to write Rust for years without naming it, yet it's quietly doing important work every time a match arm calls panic! or a let binding uses return in the else. This tutorial explains what ! is, the coercion rule that makes it useful, and how Infallible uses the same idea to say "this error can never happen", tying together threads from pattern matching, error handling, and enums.
What ! Means: An Expression With No Value
! is the type of an expression that diverges, one that never returns control to its caller. A function returning ! promises it will never return normally: it loops forever, panics, or exits the process.
// this function's return type is `!` — it never hands control back
fn fatal(msg: &str) -> ! {
eprintln!("fatal: {msg}");
std::process::exit(1); // diverges — nothing after a call to fatal() runs
}
fn parse_or_die(s: &str) -> i32 {
match s.parse() {
Ok(n) => n,
Err(_) => fatal("not a number"), // returns !, but that's fine here — see below
}
}
The value of ! is that there are no values of type !, it's uninhabited. That sounds useless, but it's precisely what lets the compiler reason about code that can't be reached, and it's why panic! and friends slot seamlessly into expressions that expect a real value.
The Coercion Rule: ! Becomes Any Type
Here's the mechanism that makes ! matter in everyday code: the never type coerces to any other type. Since a diverging expression never produces a value, the compiler can safely pretend it has whatever type the context needs, there's no actual value to be wrong about.
let count: i32 = match maybe_parse() {
Some(n) => n, // this arm yields i32
None => panic!("missing"),// this arm yields ! — coerces to i32 so the match type-checks
};
// same reason `return`/`break`/`continue` work inside expressions:
let x: u32 = if ready { compute() } else { return }; // `return` is !, coerces to u32
Without this rule, match arms and if/else branches would need every arm to produce the same real type, and you couldn't put a panic!, return, or break in one arm while the others yield a value. The never type is what quietly unifies those branches: a panic! arm "has type i32" (or whatever's needed) purely because ! fits anywhere. This is also why let x = value?; and let Some(x) = opt else { return }; (Pattern Matching Deep Dive) compile, the diverging branch coerces to match.
The Divergence Macros: panic!, unreachable!, todo!
Several standard macros evaluate to !, and choosing the right one communicates why the code diverges, which matters when someone hits it:
match status {
Status::Active => handle(),
Status::Inactive => skip(),
// pick the macro that says WHY this point is unreachable / unfinished:
Status::Deleted => unreachable!("deleted items are filtered earlier"),
}
fn not_done_yet() -> Config {
todo!() // compiles now, panics with "not yet implemented" if actually called
}
panic!— an unrecoverable error at runtime (the general case).unreachable!— a branch you've proven can't happen; panics with a clear message if your reasoning was wrong.todo!/unimplemented!— a placeholder that type-checks (returns!, so it fits any signature) but panics if reached.todo!signals "coming soon";unimplemented!signals "intentionally not provided."
Because all of these are !, you can drop them into any expression or function body regardless of the expected type, which is what makes todo!() such a useful scaffold: stub a function's body with todo!() and the signature type-checks immediately, letting you build the rest before filling it in.
Infallible: "This Error Can Never Happen"
The never type has a stable, nameable cousin used in the type system: std::convert::Infallible, an enum with no variants (so, like !, it can never be constructed). It's the idiomatic error type for a fallible interface that, for a given implementor, cannot actually fail, most often a TryFrom (TryFrom and Fallible Conversions) whose conversion is really infallible:
use std::convert::Infallible;
struct Meters(f64);
impl TryFrom<f64> for Meters {
type Error = Infallible; // this conversion never fails
fn try_from(v: f64) -> Result<Meters, Infallible> {
Ok(Meters(v)) // always Ok — Err is unconstructable
}
}
Because Infallible has no values, a Result<T, Infallible> is statically guaranteed to be Ok, the Err case can never exist. This lets a type satisfy a Result-returning trait when it genuinely can't fail, and callers can unwrap() such a result knowing it can never panic. (Infallible exists as a stable stand-in because the true ! type isn't yet usable in every position; the two are being unified in the language.)
Where the Never Type Shows Up
| Construct | Has type ! because |
|---|---|
panic! / unreachable! / todo! / unimplemented! | it aborts the current flow |
return / break / continue | control leaves the expression |
std::process::exit(n) / loop {} (no break) | it never returns |
a function declared -> ! | it's promised never to return |
Infallible (the nameable cousin) | it's an uninhabited type — no value exists |
The mental model: ! is the type of "this never yields a value," and it coerces into any type because there's no value that could contradict the context. You rarely write ! yourself (beyond a -> ! helper), but it's the invisible glue letting panic!, return, and ? live inside value-producing expressions, and Infallible is its practical face for "this fallible interface cannot fail."
Key Takeaways
!, the never type, is the type of a diverging expression, one that never returns control (panic!,return,loop {},-> !functions). It is uninhabited: no value of type!exists.- The key rule:
!coerces to any type. That's what lets amatcharm orifbranch usepanic!/return/breakwhile the other arms yield a real value, the diverging branch fits whatever type is needed. panic!,unreachable!,todo!, andunimplemented!all evaluate to!; pick the one that documents why control diverges.todo!()type-checks any signature, so it's ideal scaffolding.Infallibleis the nameable, uninhabited error type for a fallible interface (likeTryFrom) that can't actually fail;Result<T, Infallible>is statically alwaysOk.- You seldom write
!directly, but it's the glue that makespanic!,return, and?usable inside value-producing expressions.