The type that never returns
You are building a command-line tool. You write a function to parse a port number from the arguments. If the user provides a valid number, the function returns it. If the user provides garbage, the program should abort immediately with a fatal error.
You write a match statement. One arm parses the string and returns a u16. The other arm calls panic!(). The compiler rejects the code. It complains that the branches return different types. One branch produces a u16. The other branch produces... nothing? Or something else? You need a way to tell the compiler that the error branch never comes back, so the type of that branch doesn't matter.
Rust has a type for this. It is the never type, written as !. It represents expressions that diverge. They never produce a value because they panic, loop forever, or exit the process. The never type is the compiler's escape hatch for code paths that end without returning.
What the never type actually is
The never type is a bottom type. In type theory, a bottom type has zero values. You cannot construct a value of type !. You can only produce ! by writing code that never finishes.
Think of ! as a one-way door. You can walk through the door, but you never come back out. Because you never come back, the compiler can pretend you returned whatever type the caller needed. The code after the call never runs, so the type mismatch is impossible to observe.
This makes ! a subtype of every other type. If a function expects a String, and you pass it an expression of type !, the compiler accepts it. The expression will panic or loop before it could ever provide a String, but the type system allows the coercion because the value never materializes.
The never type is not (). The unit type () is a concrete type with exactly one value: the empty tuple. A function returning () returns a value, even if that value carries no data. A function returning ! returns nothing. It diverges.
/// This function returns the unit type.
/// It produces a value, the empty tuple, and returns control.
fn does_nothing() -> () {
// The body is empty, but it still returns ().
// The caller continues execution immediately.
}
/// This function returns the never type.
/// It panics and never returns control to the caller.
fn crashes() -> ! {
panic!("This function never returns");
}
The never type marks code that vanishes. Use it when the function refuses to come back.
Minimal example
The never type appears in two main forms: functions that diverge and expressions that diverge.
/// A function that panics immediately.
/// The return type is ! because the function never returns.
fn fatal_error() -> ! {
// panic! is a macro that diverges.
// It returns !, which satisfies the function signature.
panic!("Fatal error occurred");
}
/// A function that loops forever.
/// An infinite loop without a break has type !.
fn spin_forever() -> ! {
loop {
// The compiler analyzes the loop.
// There is no break statement.
// The loop never terminates, so the type is !.
}
}
/// Using the never type in a variable.
/// You can assign a diverging expression to a variable of type !.
fn main() {
// This assignment is valid.
// The variable `x` has type !.
// You can never read `x` because the assignment never completes.
let x: ! = panic!("Boom");
}
The never type is the universal adapter for diverging code. The compiler accepts it anywhere because the value never materializes.
The coercion magic
The real power of ! is coercion. Because ! is a subtype of every type, the compiler automatically coerces ! to any other type when needed. This makes control flow expressions like match much less verbose.
Consider a function that returns an i32. You want to handle an error case by panicking. Without the never type, the compiler would struggle to reconcile the types.
fn get_number(flag: bool) -> i32 {
if flag {
42
} else {
// panic! returns !.
// The compiler coerces ! to i32.
// The whole if expression has type i32.
panic!("Flag must be true")
}
}
The panic! macro returns !. The if expression requires both branches to have the same type. The compiler sees 42 (type i32) and panic!() (type !). It coerces ! to i32. The expression works.
This coercion is why match arms with panic! work seamlessly.
fn parse_port(input: &str) -> u16 {
// The match expression must return a single type.
// The Ok arm returns u16.
// The Err arm returns ! via panic!.
// ! coerces to u16, so the match type is u16.
input.parse().map_err(|_| panic!("Invalid port number"))
}
If panic! returned () instead of !, this code would fail. The compiler would see one arm returning u16 and the other returning (). Those types do not match. You would need explicit type annotations or if let chains to work around the mismatch. The never type eliminates that friction.
Coercion makes ! the universal adapter for diverging code. The compiler accepts it anywhere because the value never materializes.
Realistic usage: Control flow and assertions
The never type shows up in real Rust code in three common patterns: fatal errors, unreachable code, and scaffolding.
Unreachable code
When you write logic where a case is impossible, you can use unreachable!(). This macro returns !. It tells the compiler that the code path should never execute. If it does, the program panics with a debug assertion failure.
fn get_first(items: &[i32]) -> i32 {
match items.first() {
Some(&val) => val,
None => {
// Based on the calling logic, the list is never empty.
// If we reach this arm, there is a bug in the program.
// unreachable! returns !, which coerces to i32.
unreachable!("List should never be empty here")
}
}
}
The unreachable! macro is a promise to the compiler. You are asserting that the code is impossible. The compiler uses this information to optimize code generation, sometimes eliminating dead branches entirely.
Process exit
When you need to terminate the process with a specific exit code, use std::process::exit. This function returns !.
use std::process;
fn handle_command(cmd: &str) -> String {
match cmd {
"help" => "Usage: tool [command]".to_string(),
"exit" => {
// exit returns !.
// The program terminates immediately.
// The ! coerces to String, satisfying the return type.
process::exit(0)
}
_ => panic!("Unknown command"),
}
}
The exit function never returns. The match arm has type !. The compiler coerces ! to String. The function signature remains clean.
Scaffolding with todo!
When you are sketching out code and haven't implemented a branch yet, use todo!(). This macro returns !. It allows you to compile the code while marking incomplete sections.
fn process_data(kind: u8) -> Vec<u8> {
match kind {
1 => vec![1, 2, 3],
2 => vec![4, 5, 6],
_ => {
// Not implemented yet.
// todo! returns !, which coerces to Vec<u8>.
// The code compiles, but panics if this branch runs.
todo!("Handle unknown kind")
}
}
}
Convention aside: Use todo!() for code you plan to implement soon. Use unimplemented!() for variants that are intentionally not supported. Both return !, but the message signals different intent to other developers.
Treat unreachable! as a promise to the compiler. If the code runs, your logic is broken.
Pitfalls and compiler errors
The never type is simple, but a few traps exist.
Confusing ! with Result
The never type is not an error type. It represents divergence, not recoverable failure. If a function can fail but the caller should handle the error, return Result<T, E>. If you use ! or panic! for recoverable errors, you force the caller to deal with a crashed program instead of an error value.
/// BAD: Using ! for a recoverable error.
/// The caller cannot handle this. The program crashes.
fn read_config_bad() -> ! {
panic!("Config missing");
}
/// GOOD: Using Result for a recoverable error.
/// The caller can check the result and decide what to do.
fn read_config_good() -> Result<String, std::io::Error> {
// Return an error value.
Err(std::io::Error::new(std::io::ErrorKind::NotFound, "Config missing"))
}
Loop types depend on breaks
The type of a loop expression depends on whether it breaks. A loop without break has type !. A loop with break has the type of the break value.
// This loop never breaks. The type is !.
let diverge = loop {
// Infinite loop.
};
// This loop breaks. The type is i32.
let number = loop {
break 42;
};
If you add a break to a loop that was previously !, the type changes. This can trigger type errors in surrounding code.
fn example() -> i32 {
// This loop has type !.
// The compiler coerces ! to i32.
loop { }
}
fn example_fixed() -> i32 {
// This loop has type i32.
// No coercion needed.
loop {
break 0;
}
}
Type inference failures
In rare cases, the compiler cannot infer the target type for coercion. If ! appears in a context where the expected type is ambiguous, you get a type error.
The compiler rejects this with E0308 (mismatched types) or an inference error when it cannot determine what type ! should coerce to. This happens in generic contexts or when multiple branches diverge.
fn ambiguous() {
// The compiler cannot infer the type of the match.
// Both arms return !.
// There is no context to coerce to.
match true {
true => panic!("A"),
false => panic!("B"),
};
}
The fix is to provide a type annotation or ensure at least one arm returns a concrete type.
fn ambiguous_fixed() {
// Explicitly annotate the type.
let result: i32 = match true {
true => panic!("A"),
false => panic!("B"),
};
}
Don't use ! for errors you can recover from. Result keeps the program alive. ! kills it.
Decision: When to use the never type
The never type is a tool for control flow, not data representation. Choose it based on whether the code returns.
Use ! when you are writing a function that never returns, such as a fatal error handler or an infinite loop. Use ! when you want to assert that a code path is impossible with unreachable!(). Use ! when you are scaffolding code with todo!() or unimplemented!(). Use std::process::exit when you need to terminate the process with a specific exit code; it returns !.
Use () when the function returns a value, even if that value is empty, and the caller continues execution. Use () for side-effect functions like print! or update_state.
Use Result<T, E> when the function can fail but the caller should handle the error gracefully. Use Result for I/O operations, parsing, and network requests.
Use Option<T> when a value might be absent but the program can continue. Use Option for lookups, optional fields, and nullable data.
Match the type to the behavior. If the function returns, the type must be concrete.