How to Use Never Type (!) in Rust

The never type (!) in Rust indicates a function that never returns, used for diverging control flow like panics or infinite loops.

The type that never is

You are writing a command parser. The function takes a string and returns an integer code. Most commands map cleanly to numbers. The default case should crash the program because an unknown command is a fatal error. You write a match expression. The start arm returns 1. The stop arm returns 0. The wildcard arm calls panic!("Unknown command").

The compiler accepts this without complaint. It knows the panic! branch never produces a value, so it doesn't force the whole expression to be type (). It lets the expression resolve to i32 based on the other arms. This works because panic! returns the never type, written as !.

The never type represents a computation that never completes. It has no values. You cannot hold a variable of type !. You cannot return a value of type !. The only way to produce a ! is to diverge: panic, loop forever, or exit the process. The type exists to tell the compiler that a code path vanishes, allowing type checking and inference to work correctly in the surrounding code.

How coercion saves the day

The magic of ! is not just that it marks divergence. It is that ! coerces to any other type. In type theory terms, ! is a subtype of every type T. If a context expects an i32, and you provide a !, the compiler accepts it. The logic is simple: code after a ! is unreachable, so the type of the value doesn't matter. The value never exists, so it can safely pretend to be anything.

This coercion enables seamless integration of diverging code into expressions. Without it, every if branch or match arm that panics would require a type cast or a wrapper. With !, the compiler infers the correct type from the surviving branches and treats the diverging branches as transparent.

fn get_status_code(success: bool) -> i32 {
    if success {
        200
    } else {
        // panic! returns !.
        // The compiler coerces ! to i32 because the other arm is i32.
        // This branch never produces a value, so the coercion is safe.
        panic!("Request failed")
    }
}

The coercion happens implicitly. You never write panic!() as i32. The compiler inserts the coercion automatically. This keeps your code clean and focuses on logic rather than type annotations.

Type inference superpower

The never type is a silent hero for type inference. When you write an expression with multiple branches, the compiler must determine a single type for the whole expression. If one branch diverges, ! allows the compiler to ignore that branch for type inference and pick the type from the other branches.

Consider a configuration loader. You want to return a Config object. If the config file is missing, you panic. The panic branch returns !. The success branch returns Config. The compiler infers the expression type as Config. If panic! returned () instead, the compiler would see a conflict between () and Config and reject the code. You would have to annotate the type explicitly or restructure the code.

fn load_config() -> Config {
    let data = std::fs::read_to_string("config.json")
        .expect("Config file must exist");
    
    // serde_json::from_str returns Result<Config, Error>.
    // unwrap_or_else takes a closure that must return Config.
    // The closure calls panic!, which returns !.
    // Coercion converts ! to Config, satisfying the closure signature.
    serde_json::from_str(&data).unwrap_or_else(|e| {
        eprintln!("Parse error: {}", e);
        panic!("Invalid config format")
    })
}

The unwrap_or_else closure must return Config. The panic! call returns !. Coercion bridges the gap. This pattern appears constantly in Rust code. Error handling combinators, match arms, and conditional expressions all rely on ! to keep types consistent without boilerplate.

Real-world patterns

You encounter ! in three main places: explicit function signatures, diverging expressions, and unsafe hints.

Explicit signatures document intent. When a function is designed to never return, annotate it with -> !. This tells callers that the function diverges. It also helps the compiler optimize code paths that call the function, since it knows control never flows back.

/// Terminates the program with a fatal error message.
/// This function never returns, so the return type is !.
fn fatal(msg: &str) -> ! {
    eprintln!("FATAL: {}", msg);
    std::process::exit(1);
}

fn main() {
    let x: i32 = if true {
        // fatal returns !.
        // Coercion allows this branch to match the i32 type of the else branch.
        fatal("Something went wrong")
    } else {
        42
    };
    
    // This line is unreachable because the if branch always diverges.
    println!("{}", x);
}

Diverging expressions appear in loops and matches. An infinite loop has type !. A match arm that panics has type !. These expressions integrate naturally into larger code structures thanks to coercion.

fn wait_for_signal() -> ! {
    loop {
        // The loop body runs forever.
        // The loop expression has type !.
        // This satisfies the function signature.
        std::thread::sleep(std::time::Duration::from_secs(1));
    }
}

The unsafe hint std::hint::unreachable_unchecked() also returns !. This function tells the compiler that the current code path is impossible. If you are wrong, the program exhibits undefined behavior. Use this only when you have proven mathematically that the path cannot be reached, and when the performance benefit justifies the risk.

fn get_first_element(slice: &[i32]) -> i32 {
    match slice.split_first() {
        Some((first, _rest)) => *first,
        None => {
            // SAFETY: The function contract guarantees a non-empty slice.
            // Callers must enforce this. If the slice is empty, this is UB.
            // unreachable_unchecked returns !, coercing to i32.
            unsafe { std::hint::unreachable_unchecked() }
        }
    }
}

Convention dictates that unreachable_unchecked lives inside a small unsafe block with a // SAFETY: comment listing the invariants. The community treats this as a proof obligation. If you cannot write the proof, do not use the hint. Stick to panic! or unreachable!, which are safe and produce a runtime crash instead of undefined behavior.

Pitfalls and compiler errors

The never type introduces specific pitfalls. The most common error is promising ! but delivering a value. If you annotate a function with -> ! and return a concrete value, the compiler rejects you with E0308 (mismatched types). The compiler expects a diverging expression, not a value.

fn bad_diverge() -> ! {
    // E0308: expected `!`, found `i32`
    // The function promised never to return, but it returns 42.
    42
}

Fix this by removing the ! annotation if the function returns normally, or by adding a diverging expression like panic!() or loop {} if the function truly never returns.

Another pitfall is trying to use a value of type !. Since ! has no values, you cannot print it, pass it to a function, or store it in a variable that you later use. If you try to use a ! value, the compiler complains with E0277 (trait bound not satisfied) or E0308, depending on the context.

fn main() {
    let x: ! = panic!("Never");
    
    // E0277: the trait bound `!: std::fmt::Display` is not satisfied
    // You cannot print a value of type ! because no such value exists.
    println!("{}", x);
}

The compiler catches this because x has type !. The println! macro requires the argument to implement Display. The never type does not implement any traits. This error usually indicates a logic mistake: you are trying to use a value that can never exist.

A subtle pitfall involves generic functions. If a generic function returns T, and you call it with T = !, the result is !. This is valid, but you cannot use the result. Some generic code may break if it assumes T has values or implements traits. Be cautious when passing ! as a type parameter.

Decision matrix

Use ! for functions that never return, such as those that call panic!, process::exit, or loop infinitely. Use () for functions that return control but produce no meaningful value, like a procedure that prints a message or updates state. Use Result when a function can fail recoverably and the caller should handle the error. Use Option when a value might be absent but the absence is not an error. Reach for unreachable_unchecked only when you have a formal proof that a path is impossible and profiling shows the branch prediction cost is significant; otherwise, use unreachable!() or panic!() for safety.

Where to go next