The Never Type
The never type, written !, is the type of computations that never produce a value, such as panic!, continue, break from a never-returning context, process exit, and endless loops without break.
What it is
! is an uninhabited type: no value of type ! can exist.
Functions returning ! are called diverging functions because control never returns normally to the caller.
The practical reason ! matters is type coercion.
An expression of type ! can coerce to another type because it never actually has to provide a value of that type.
This is why a match arm that says continue can live beside another arm that returns a u32.
panic! also has type !.
That lets APIs such as Option::unwrap return T: the Some(value) branch yields T, and the None branch diverges.
How it works
Rust requires expression branches to agree on a type. When one branch diverges, the compiler can use the type of the branch that does return. The diverging branch cannot violate the chosen type because execution never reaches a point where a value is needed.
Use ! explicitly when a function is deliberately non-returning: a fatal error helper, a command that exits the process, or a loop that owns the thread forever.
For ordinary errors, prefer Result and The Question Mark Operator.
The compiler treats ! as an uninhabited type, so pattern matching on a value of type ! would require no arms.
In stable Rust 1.85, explicit ! is primarily written in function return positions; you usually encounter it through diverging expressions such as panic!, return, break, continue, and infinite loops.
That distinction matters when reading diagnostics: the compiler may infer a diverging expression in a larger expression even when you did not write ! yourself.
The coercion is one-way.
An expression that actually produces a value cannot be treated as !; only an expression that never completes can stand in for another type.
Example
fn parse_or_skip(input: &str) -> Option<u32> {
loop {
let number: u32 = match input.trim().parse() {
Ok(value) => value,
Err(_) => break None,
};
break Some(number);
}
}
fn fatal(message: &str) -> ! {
panic!("{message}");
}
fn main() {
assert_eq!(parse_or_skip("42"), Some(42));
assert_eq!(parse_or_skip("nope"), None);
let enabled = true;
let value: u32 = if enabled { 10 } else { fatal("disabled") };
assert_eq!(value, 10);
}More realistic example
use std::process;
fn require_env(name: &str) -> String {
match std::env::var(name) {
Ok(value) => value,
Err(error) => exit_with_message(&format!("{name} is required: {error}")),
}
}
fn exit_with_message(message: &str) -> ! {
eprintln!("{message}");
process::exit(2);
}
fn main() {
// The Err arm has type !, so the match expression still has type String.
let _path = require_env("PATH");
}This is appropriate in a binary boundary where the program really should terminate.
Inside a library, return Result<String, VarError> instead so the caller decides how to handle failure.
Common errors
fn main() {
let value = match "42".parse::<u32>() {
Ok(number) => number,
// Err(_) => "not a number",
Err(_) => panic!("not a number"),
};
assert_eq!(value, 42);
}If the commented string arm is used, the typical diagnostic is:
error[E0308]: match arms have incompatible types.
The fix is not always to panic!; usually choose one return type such as Result<u32, ParseIntError> or Option<u32>.
Use a diverging arm only when the branch should truly never continue.
Another common surprise is an accidental non-diverging loop:
fn forever() -> ! {
loop {
if false {
break; // error[E0308]: mismatched types, expected `!`, found `()`
}
}
}Remove the reachable break, return a value type such as (), or make the loop break with a value in a function whose return type matches that value.
Best practice
- ✅ Use explicit
-> !for functions whose whole contract is “does not return normally.” - ✅ Let diverging control flow simplify
matchandifexpressions when rejecting, skipping, or exiting a branch. - ✅ Use Result for recoverable failure; reserve
panic!or process exit for unrecoverable states and binary entry points. - ✅ Treat
loop { ... }as!only when the loop truly cannotbreak. - ✅ Prefer
return Err(...)or?over a fatal helper when writing library code. - ✅ Use
std::process::exitonly at the edge of a command-line program; it skips normal unwinding and does not run all destructors.
Pitfalls
- ⚠️ Do not use
panic!merely to satisfy a type checker; it turns a recoverable design question into runtime failure. See Unwrap and Expect Overuse. - ⚠️ Remember that
!means no value is produced, not “unknown type” or “any value.” - ⚠️ A
loopwith a reachablebreak valuehas the type of that break value, not!. - ⚠️ Avoid teaching
!as a normal substitute for every type; the coercion works because there is no runtime value to inspect. - ⚠️ Do not model business-state impossibility with
panic!; use enums and exhaustive Pattern Matching where the state is part of normal control flow.
See also
panic! · Result · The Question Mark Operator · Option vs Result · Unwrap and Expect Overuse · Recoverable vs Unrecoverable Errors · Pattern Matching · Control Flow · Advanced Types & Features
Sources
- The Rust Programming Language, ch. 20.3 “The Never Type That Never Returns” — the-book, https://doc.rust-lang.org/book/ch20-03-advanced-types.html#the-never-type-that-never-returns
- The Rust Reference, “Never type” — the-reference, https://doc.rust-lang.org/reference/types/never.html
