Skip to content

Rust Error E0004: Non-exhaustive Patterns Not Covered

E0004 means your match forgot at least one possible value, and Rust refuses to guess what should happen in that case. It’s one of the friendliest errors in Rust: the compiler tells you exactly which case is missing and even writes the line for you. This page shows the fix first, then explains why this check is one of Rust’s best features. For the other errors in this series, see Rust compiler errors.

error[E0004]: non-exhaustive patterns: `OrderStatus::Delivered` not covered
--> src/main.rs:8:11
|
8 | match status {
| ^^^^^^ pattern `OrderStatus::Delivered` not covered
|
note: `OrderStatus` defined here
--> src/main.rs:1:6
|
1 | enum OrderStatus {
| ^^^^^^^^^^^
...
4 | Delivered, // added later
| --------- not covered
= note: the matched value is of type `OrderStatus`
help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown
|
10 ~ OrderStatus::Shipped => "on its way",
11 ~ OrderStatus::Delivered => todo!(),
|

This is the program that produces it:

enum OrderStatus {
Pending,
Shipped,
Delivered, // added later
}
fn describe(status: OrderStatus) -> &'static str {
match status {
OrderStatus::Pending => "waiting to be packed",
OrderStatus::Shipped => "on its way",
// ❌ nothing for Delivered
}
}
fn main() {
println!("{}", describe(OrderStatus::Delivered));
}

The text between the backticks changes with your code. You may see `None` not covered, `&_` not covered, `0_u8` and `4_u8..=u8::MAX` not covered or `(true, false)` and `(false, true)` not covered. It’s the same error each time: Rust is listing the values your match has no arm for.

Read the message from top to bottom:

  1. Line 8: the ^^^^^^ points at the value you’re matching on, status.
  2. “pattern OrderStatus::Delivered not covered”: this is the case you forgot.
  3. The note jumps to the enum definition and marks the forgotten variant on line 4.
  4. The help shows the exact arm to add. todo!() is a placeholder; you replace it with real code.

Think of a vending machine with three buttons, where only two are wired up. Pressing the third one would do… what? Rust won’t let you ship a machine with a dead button. Every button needs an answer, even if the answer is “out of order”.

A match in Rust must be exhaustive: for every value the type can hold, exactly one arm must apply. If an arm were missing, the program would reach a point with no code to run. Other languages quietly fall through, return a default, or crash at runtime. Rust catches it before the program ever runs. The match lesson covers the basics.

The real payoff comes later, when your code grows. Say you add a new variant to an enum that is matched in ten places across your project:

enum OrderStatus {
Pending,
Shipped,
Delivered,
Returned, // new!
}

The compiler now gives you one E0004 for every match that needs updating:

error[E0004]: non-exhaustive patterns: `&OrderStatus::Returned` not covered
--> src/main.rs:9:11
error[E0004]: non-exhaustive patterns: `&OrderStatus::Returned` not covered
--> src/main.rs:17:11

That’s a to-do list written for you. You don’t have to search the code base and hope you found every spot. This is why experienced Rust developers like enums so much: you can change them without fear.

Choose this for your own enums. This is the right fix most of the time, and it’s exactly what rustc’s help: suggests. Add the arm, and replace todo!() with what should really happen:

enum OrderStatus {
Pending,
Shipped,
Delivered,
}
fn describe(status: OrderStatus) -> &'static str {
match status {
OrderStatus::Pending => "waiting to be packed",
OrderStatus::Shipped => "on its way",
OrderStatus::Delivered => "at your door", // ✅ every case handled
}
}
fn main() {
println!("{}", describe(OrderStatus::Pending));
println!("{}", describe(OrderStatus::Delivered));
}

Output:

waiting to be packed
at your door
▶ Try it Yourself

If several variants should do the same thing, join them with | in one arm: OrderStatus::Shipped | OrderStatus::Delivered => false. You still name each variant, so the compiler can still warn you about new ones. The same goes for Option: if the error says `None` not covered, add a None => ... arm. See Option.

Fix 2: Use ranges and _ for numbers and text

Section titled “Fix 2: Use ranges and _ for numbers and text”

Choose this when you match on integers, char, or strings. A u8 has 256 possible values and a &str has endless ones, so you can’t list them all. Use ranges like 1..=9 for groups of numbers, and finish with _ (or a name like other) for “everything else”:

fn grade(score: u8) -> char {
match score {
90..=100 => 'A',
75..=89 => 'B',
50..=74 => 'C',
0..=49 => 'F',
_ => '?', // 101 to 255: not a real score
}
}
fn run(command: &str) {
match command {
"start" => println!("Starting the timer"),
"stop" => println!("Stopping the timer"),
other => println!("Unknown command: {other}"),
}
}
fn main() {
println!("{} {} {}", grade(95), grade(62), grade(150));
run("start");
run("pause");
}

Output:

A C ?
Starting the timer
Unknown command: pause
▶ Try it Yourself

For strings, rustc even says it directly: `&str` cannot be matched exhaustively, so a wildcard `_` is necessary. For numbers, Rust checks your ranges precisely. If they really cover every value (for example 0..=49 up to 90..=u8::MAX), you don’t need _ at all. More in Matching a range.

Fix 3: Use if let or let else when you care about one case

Section titled “Fix 3: Use if let or let else when you care about one case”

Choose this when you only want to act on one pattern and ignore the rest. A full match with an empty “do nothing” arm works, but if let says it more clearly. let else is for “get the value out, or leave the function early”:

fn greet(nickname: Option<&str>) {
if let Some(name) = nickname {
println!("Hi, {name}!");
}
// no nickname: do nothing
}
fn first_word_len(text: &str) -> usize {
let Some(word) = text.split_whitespace().next() else {
return 0; // empty text: leave early
};
word.len()
}
fn main() {
greet(Some("Ferris"));
greet(None);
println!("{}", first_word_len("hello world"));
println!("{}", first_word_len(" "));
}

Output:

Hi, Ferris!
5
0
▶ Try it Yourself

The else block of a let else must leave the current block (return, break, continue or a panic). See if let and let-else.

Fix 4: A _ => catch-all for the rest (with a trade-off)

Section titled “Fix 4: A _ => catch-all for the rest (with a trade-off)”

Choose this when most variants of an enum really do the same thing. _ matches anything that the arms above it didn’t:

enum Key {
Up,
Down,
Left,
Right,
Enter,
Escape,
}
fn handle(key: Key) {
match key {
Key::Enter => println!("Confirm"),
Key::Escape => println!("Cancel"),
_ => println!("Move the cursor"),
}
}
fn main() {
handle(Key::Enter);
handle(Key::Left);
handle(Key::Escape);
}

Output:

Confirm
Move the cursor
Cancel
▶ Try it Yourself

Be honest about the cost: _ switches off the check from Why Rust does this. If you later add Key::Delete, the compiler stays silent, and pressing Delete moves the cursor. That’s a bug Rust could have caught for you. For your own enums, prefer listing the variants with | (Key::Up | Key::Down | Key::Left | Key::Right => ...). Save _ for numbers, strings, and enums from other crates.

Option without None. match nickname { Some(name) => ... } gives `None` not covered. Fix: add a None => arm, or use if let Some(name) = nickname if there’s nothing to do for None.

Tuples and booleans. Matching two bools at once has four combinations, and it’s easy to forget some:

match (logged_in, is_admin) {
(true, true) => println!("Welcome, admin"),
(false, false) => println!("Please log in"),
} // ❌ patterns `(true, false)` and `(false, true)` not covered

Fix: add the missing arms, or use _ inside the tuple, like (false, _) => println!("Please log in"), which covers both “logged out” cases. See Matching multiple values at once.

A pattern in let. Writing let Some(score) = saved; is a let that might not match. This is a sister error, E0005, with the same idea:

error[E0005]: refutable pattern in local binding
--> src/main.rs:3:9
|
3 | let Some(score) = saved;
| ^^^^^^^^^^^ pattern `None` not covered
|
= note: `let` bindings require an "irrefutable pattern", like a `struct` or an `enum` with only one variant
help: you might want to use `let...else` to handle the variant that isn't matched
|
3 | let Some(score) = saved else { todo!() };
| ++++++++++++++++

Fix: let Some(score) = saved else { return; };, or if let, as in Fix 3. See Patterns in let.

Enums from other crates. Some enums, like std::io::ErrorKind, are marked #[non_exhaustive]. Their authors may add variants later, so even if you list every variant you see today, rustc says `_` not covered. Fix: add a _ => arm. Here it’s required, not a shortcut.

  • E0005 (refutable pattern in local binding): the same “not covered” check, for let instead of match. Shown above.
  • E0308: mismatched types, for example when match arms return different types ("text" in one arm, a number in another).
  • E0382: borrow of partially moved value, when a match arm like Some(name) moves a String out and you use the original afterwards. Match on &nickname instead.
  • E0507: cannot move out of a borrowed value, when a match arm tries to take a String out of something you only borrowed.
  • All compiler errors: the full list of error pages.

Quick check

1. You add a variant Refunded to an enum. What does the compiler do with your existing match expressions that list every variant by name?

2. You match a &str against "yes" and "no". How do you fix E0004?

3. What is the downside of _ => in a match on your own enum?

Score: 0 / 3