Skip to content

Rust Error E0005: Refutable Pattern in Local Binding

E0005 means you wrote a let whose pattern might not match, like let Some(x) = value; when value could be None. Rust needs to know what happens in that case, and a plain let has no place to say it. The fix is to give the “didn’t match” case somewhere to go: usually let ... else, if let, or match. This is a very common error when you start working with Option and Result, and it takes a minute to fix. For the other errors in this series, see Rust compiler errors.

error[E0005]: refutable pattern in local binding
--> src/main.rs:4:9
|
4 | let Some(top) = best; // ❌ what if the list was empty?
| ^^^^^^^^^ pattern `None` not covered
|
= note: `let` bindings require an "irrefutable pattern", like a `struct` or an `enum` with only one variant
= note: for more information, visit https://doc.rust-lang.org/book/ch19-02-refutability.html
= note: the matched value is of type `Option<i32>`
help: you might want to use `let...else` to handle the variant that isn't matched
|
4 | let Some(top) = best else { todo!() }; // ❌ what if the list was empty?
| ++++++++++++++++

This is the program that produces it:

fn main() {
let scores = vec![72, 95, 88];
let best = scores.iter().max().copied();
let Some(top) = best; // ❌ what if the list was empty?
println!("Top score: {top}");
}

You’ll see the same error with other patterns. Only the “not covered” part changes:

3 | let Ok(age) = input.parse::<i32>();
| ^^^^^^^ pattern `Err(_)` not covered
3 | let [host, port] = parts.as_slice();
| ^^^^^^^^^^^^ patterns `&[]`, `&[_]` and `&[_, _, _, ..]` not covered

Two words do the work here:

  • A refutable pattern can fail to match. Some(top) fails when the value is None. Ok(age) fails on Err. [host, port] fails when the slice doesn’t have exactly two items.
  • An irrefutable pattern always matches. A plain name like let top = best; always works, and so do tuples and structs like let (x, y) = point;.

A let must always succeed, because the next line uses the variables it creates. If best were None, what would top be on the next line? There’s no answer, so Rust stops you. The “not covered” label lists exactly the cases you forgot: None, Err(_), or for the slice, “empty”, “one item” and “three or more items”.

Think of a parcel locker. let Some(top) = best; is like walking away sure that a parcel is inside, without opening the door. Rust makes you decide what to do if the locker is empty.

let, function parameters and for loops all need patterns that can’t fail. match, if let, while let and let ... else accept patterns that can fail, because each of them has a place for the other case. This is the same rule as E0004, where a match must cover every value. Here the “match” is a let with just one arm.

In many languages, reading a value that isn’t there gives you null, undefined or a crash at runtime. In Rust, an empty list, a bad number in a text field or a missing setting are normal cases you handle in code. Option and Result make them visible, and E0005 makes sure you don’t skip them by accident. See Patterns in let and Option.

Choose this when the rest of the function needs the value, and without it there’s nothing useful to do. It’s what rustc’s help: suggests. You keep your pattern and add an else block. That block must leave: return, break, continue or a panic.

fn port_from(text: &str) -> u16 {
// get the number out, or leave the function early
let Ok(port) = text.trim().parse::<u16>() else {
println!("'{text}' is not a valid port, using 8080");
return 8080;
};
port
}
fn main() {
let scores = vec![72, 95, 88];
let Some(top) = scores.iter().max() else {
println!("No scores yet");
return;
};
println!("Top score: {top}");
println!("Port: {}", port_from("3000"));
println!("Port: {}", port_from("abc"));
}

Output:

Top score: 95
Port: 3000
'abc' is not a valid port, using 8080
Port: 8080
▶ Try it Yourself

Don’t keep rustc’s else { todo!() } as it is. todo!() panics when it runs, so replace it with a real way out. More in let-else.

Fix 2: match or if let when both cases do something

Section titled “Fix 2: match or if let when both cases do something”

Choose this when the “no match” case has its own work to do, like printing the error, or when the code continues either way. match lets you handle every case and use the error value. It’s also the way to take a slice apart when you don’t know its length. Use if let when only one case matters:

fn main() {
// match: handle both outcomes, and use the error
for input in ["42", "forty-two"] {
match input.parse::<i32>() {
Ok(age) => println!("Age: {age}"),
Err(e) => println!("Can't read '{input}': {e}"),
}
}
// a slice can have any length: match on its shape
for address in ["localhost:8080", "localhost"] {
let parts: Vec<&str> = address.split(':').collect();
match parts.as_slice() {
[host, port] => println!("host = {host}, port = {port}"),
_ => println!("'{address}' should look like host:port"),
}
}
// if let: only do something when there is a value
let nickname: Option<&str> = None;
if let Some(name) = nickname {
println!("Hi, {name}!");
} else {
println!("Hi, stranger!");
}
}

Output:

Age: 42
Can't read 'forty-two': invalid digit found in string
host = localhost, port = 8080
'localhost' should look like host:port
Hi, stranger!
▶ Try it Yourself

The _ arm catches every other length. Without it, match would give you E0004 with the same “not covered” list. See if let and match.

Fix 3: A default with unwrap_or, or pass it on with ?

Section titled “Fix 3: A default with unwrap_or, or pass it on with ?”

Choose this when there’s a sensible default value, or when the caller should deal with the problem. unwrap_or(x) gives you the value, or x if there is none. ? works inside a function that returns Result (or Option): it hands the error back to the caller and stops the function.

use std::num::ParseIntError;
// ? passes the error to the caller instead of handling it here
fn total_minutes(hours: &str, minutes: &str) -> Result<i32, ParseIntError> {
let h = hours.parse::<i32>()?;
let m = minutes.parse::<i32>()?;
Ok(h * 60 + m)
}
fn main() {
// a default value when there is nothing
let scores: Vec<i32> = Vec::new();
let top = scores.iter().max().copied().unwrap_or(0);
println!("Top score: {top}");
let volume = "loud".parse::<u8>().unwrap_or(50);
println!("Volume: {volume}");
println!("{:?}", total_minutes("2", "30"));
println!("{:?}", total_minutes("2", "half"));
}

Output:

Top score: 0
Volume: 50
Ok(150)
Err(ParseIntError { kind: InvalidDigit })
▶ Try it Yourself

If you’re sure the value is there (for example a number you just wrote in the code), .expect("reason") is fine too. It panics with your message if you were wrong, so keep it for cases that really can’t fail. Using ? in a main that returns nothing gives E0277; see The ? operator and Shortcuts: unwrap_or.

Fix 4: Use a type whose pattern can’t fail

Section titled “Fix 4: Use a type whose pattern can’t fail”

Choose this when the data always has the same shape, and the pattern only fails because of the type you picked. A slice or Vec can have any length, but an array like [u8; 3] always has exactly three items, so let [r, g, b] = color; always matches. Tuples and structs always match too. And split_once gives you both halves as one tuple, so you deal with one Option instead of a slice of unknown length:

struct Point {
x: i32,
y: i32,
}
fn main() {
// arrays have a fixed length: this pattern always matches
let color: [u8; 3] = [255, 140, 0];
let [r, g, b] = color;
println!("r={r} g={g} b={b}");
// tuples and structs always match too
let (host, port) = ("localhost", 8080);
let Point { x, y } = Point { x: 3, y: -1 };
println!("{host}:{port} at ({x}, {y})");
// split_once returns one Option with both halves
let Some((key, value)) = "theme=dark".split_once('=') else {
return;
};
println!("{key} -> {value}");
}

Output:

r=255 g=140 b=0
localhost:8080 at (3, -1)
theme -> dark
▶ Try it Yourself

The same goes for your own enums: let Shape::Circle(radius) = shape; fails with pattern `Shape::Square(_)` not covered. If the value can really only be a circle, maybe it should be a Circle struct instead of an enum. See Arrays and Structs.

A for loop over Options. for Some(temp) in readings gives “refutable pattern in for loop binding”. Fix: for temp in readings.iter().flatten() skips the Nones, or loop over every item and use if let Some(t) = reading inside.

A function parameter. fn greet(Some(name): Option<&str>) gives “refutable pattern in function argument”. Fix: take name: Option<&str> and match inside the function, or take name: &str and let the caller decide what to do with None.

Parsing input. let Ok(n) = line.trim().parse::<i32>(); when reading user input or command-line arguments. Fix: let Ok(n) = ... else { println!("Please type a number"); continue; }; inside the input loop, or match to print the error.

Quick check

1. Which let gives E0005?

2. What must the else block of a let ... else do?

3. You need a number from text, and 0 is a fine default if the text is not a number. What is the shortest fix?

Score: 0 / 3