Ir al contenido

Error E0004 de Rust: non-exhaustive patterns not covered (match incompleto)

E0004 significa que a tu match se le olvidó al menos un valor posible, y Rust se niega a adivinar qué debería pasar en ese caso. Es uno de los errores más amables de Rust: el compilador te dice exactamente qué caso falta e incluso te escribe la línea. Esta página te muestra primero la solución y después explica por qué esta comprobación es una de las mejores cosas de Rust. Para los demás errores de esta serie, mira Errores del compilador de Rust.

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, // añadido más tarde
| --------- 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!(),
|

“Non-exhaustive patterns: … not covered” quiere decir “patrones no exhaustivos: … no está cubierto”. El help te pide que añadas un brazo (arm) con un comodín o con el patrón que falta, y te enseña cómo.

Este es el programa que lo produce:

enum OrderStatus {
Pending,
Shipped,
Delivered, // añadido más tarde
}
fn describe(status: OrderStatus) -> &'static str {
match status {
OrderStatus::Pending => "waiting to be packed",
OrderStatus::Shipped => "on its way",
// ❌ nada para Delivered
}
}
fn main() {
println!("{}", describe(OrderStatus::Delivered));
}

El texto entre las comillas invertidas cambia según tu código. Puedes ver `None` not covered, `&_` not covered, `0_u8` and `4_u8..=u8::MAX` not covered o `(true, false)` and `(false, true)` not covered. Es el mismo error cada vez: Rust te está enumerando los valores para los que tu match no tiene ningún brazo.

Lee el mensaje de arriba abajo:

  1. Línea 8: el ^^^^^^ señala el valor sobre el que haces match, status.
  2. “pattern OrderStatus::Delivered not covered”: este es el caso que olvidaste.
  3. La note salta a la definición del enum y marca la variante olvidada en la línea 4.
  4. El help te enseña el brazo exacto que tienes que añadir. todo!() es un marcador provisional; lo sustituyes por código de verdad.

Piensa en una máquina expendedora con tres botones, de los que solo dos están conectados. Si alguien pulsa el tercero, ¿qué pasa? Rust no te deja entregar una máquina con un botón muerto. Cada botón necesita una respuesta, aunque la respuesta sea “fuera de servicio”.

Un match en Rust tiene que ser exhaustivo: para cada valor que puede tener el tipo, tiene que aplicarse exactamente un brazo. Si faltara un brazo, el programa llegaría a un punto sin código que ejecutar. Otros lenguajes siguen de largo sin decir nada, devuelven un valor por defecto o fallan en tiempo de ejecución. Rust lo detecta antes de que el programa llegue a ejecutarse. La lección de match explica lo básico.

La verdadera recompensa llega más tarde, cuando tu código crece. Imagina que añades una variante nueva a un enum que se usa en un match en diez sitios de tu proyecto:

enum OrderStatus {
Pending,
Shipped,
Delivered,
Returned, // ¡nueva!
}

Ahora el compilador te da un E0004 por cada match que hay que actualizar:

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

Es una lista de tareas escrita para ti. No tienes que buscar por todo el código y cruzar los dedos para haber encontrado todos los sitios. Por eso los desarrolladores de Rust con experiencia aprecian tanto los enums: puedes cambiarlos sin miedo.

Elige esta para tus propios enums. Es la solución correcta casi siempre, y es justo lo que sugiere el help: de rustc. Añade el brazo y sustituye todo!() por lo que de verdad debería pasar:

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", // ✅ todos los casos cubiertos
}
}
fn main() {
println!("{}", describe(OrderStatus::Pending));
println!("{}", describe(OrderStatus::Delivered));
}

Salida:

waiting to be packed
at your door
▶ Pruébalo tú mismo

Si varias variantes deben hacer lo mismo, únelas con | en un solo brazo: OrderStatus::Shipped | OrderStatus::Delivered => false. Sigues nombrando cada variante, así que el compilador te puede seguir avisando de las nuevas. Lo mismo vale para Option: si el error dice `None` not covered, añade un brazo None => .... Mira Option.

Solución 2: Usar rangos y _ con números y texto

Sección titulada «Solución 2: Usar rangos y _ con números y texto»

Elige esta cuando haces match sobre enteros, char o strings. Un u8 tiene 256 valores posibles y un &str tiene infinitos, así que no puedes enumerarlos todos. Usa rangos como 1..=9 para grupos de números, y termina con _ (o con un nombre como other) para “todo lo demás”:

fn grade(score: u8) -> char {
match score {
90..=100 => 'A',
75..=89 => 'B',
50..=74 => 'C',
0..=49 => 'F',
_ => '?', // de 101 a 255: no es una nota real
}
}
fn run(command: &str) {
match command {
"start" => println!("Iniciando el temporizador"),
"stop" => println!("Deteniendo el temporizador"),
other => println!("Comando desconocido: {other}"),
}
}
fn main() {
println!("{} {} {}", grade(95), grade(62), grade(150));
run("start");
run("pause");
}

Salida:

A C ?
Iniciando el temporizador
Comando desconocido: pause
▶ Pruébalo tú mismo

Con strings, rustc incluso lo dice directamente: `&str` cannot be matched exhaustively, so a wildcard `_` is necessary (un &str no se puede cubrir entero, así que hace falta un comodín _). Con números, Rust comprueba tus rangos con precisión. Si de verdad cubren todos los valores (por ejemplo, de 0..=49 hasta 90..=u8::MAX), no necesitas _ en absoluto. Más en Rangos.

Solución 3: Usar if let o let else cuando solo te importa un caso

Sección titulada «Solución 3: Usar if let o let else cuando solo te importa un caso»

Elige esta cuando solo quieres actuar sobre un patrón e ignorar el resto. Un match completo con un brazo vacío de “no hacer nada” funciona, pero if let lo dice con más claridad. let else sirve para “saca el valor, o sal de la función antes de tiempo”:

fn greet(nickname: Option<&str>) {
if let Some(name) = nickname {
println!("¡Hola, {name}!");
}
// sin apodo: no hacemos nada
}
fn first_word_len(text: &str) -> usize {
let Some(word) = text.split_whitespace().next() else {
return 0; // texto vacío: salimos antes
};
word.len()
}
fn main() {
greet(Some("Ferris"));
greet(None);
println!("{}", first_word_len("hello world"));
println!("{}", first_word_len(" "));
}

Salida:

¡Hola, Ferris!
5
0
▶ Pruébalo tú mismo

(El texto "hello world" se queda en inglés a propósito: len() cuenta bytes, y con palabras acentuadas el resultado cambiaría.) El bloque else de un let else tiene que salir del bloque actual (return, break, continue o un panic). Mira if let y let-else.

Solución 4: Un comodín _ => para el resto (con un precio)

Sección titulada «Solución 4: Un comodín _ => para el resto (con un precio)»

Elige esta cuando la mayoría de las variantes de un enum de verdad hacen lo mismo. _ coincide con todo lo que no cubrieron los brazos anteriores:

enum Key {
Up,
Down,
Left,
Right,
Enter,
Escape,
}
fn handle(key: Key) {
match key {
Key::Enter => println!("Confirmar"),
Key::Escape => println!("Cancelar"),
_ => println!("Mover el cursor"),
}
}
fn main() {
handle(Key::Enter);
handle(Key::Left);
handle(Key::Escape);
}

Salida:

Confirmar
Mover el cursor
Cancelar
▶ Pruébalo tú mismo

Sé sincero sobre el precio: _ desactiva la comprobación de Por qué Rust hace esto. Si más adelante añades Key::Delete, el compilador no dice nada, y al pulsar Suprimir se mueve el cursor. Es un bug que Rust podría haber detectado por ti. Para tus propios enums, mejor enumera las variantes con | (Key::Up | Key::Down | Key::Left | Key::Right => ...). Guarda _ para números, strings y enums de otros crates.

Option sin None. match nickname { Some(name) => ... } da `None` not covered. Solución: añade un brazo None =>, o usa if let Some(name) = nickname si no hay nada que hacer con None.

Tuplas y booleanos. Hacer match sobre dos bool a la vez da cuatro combinaciones, y es fácil olvidar alguna:

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

El comentario muestra lo que dice rustc: faltan las combinaciones (true, false) y (false, true). Solución: añade los brazos que faltan, o usa _ dentro de la tupla, como (false, _) => println!("Please log in"), que cubre los dos casos de “sin sesión iniciada”. Mira Varios valores a la vez.

Un patrón en un let. Escribir let Some(score) = saved; es un let que puede no coincidir. Es un error hermano, E0005, con la misma 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!() };
| ++++++++++++++++

La note explica que un let necesita un “patrón irrefutable”, uno que siempre coincide, y el help propone let...else para el caso que falta. Solución: let Some(score) = saved else { return; };, o if let, como en la Solución 3. Mira Patrones en let.

Enums de otros crates. Algunos enums, como std::io::ErrorKind, están marcados con #[non_exhaustive]. Sus autores pueden añadir variantes más adelante, así que aunque enumeres todas las variantes que ves hoy, rustc dice `_` not covered. Solución: añade un brazo _ =>. Aquí es obligatorio, no un atajo.

  • E0005 (refutable pattern in local binding, patrón refutable en un let): la misma comprobación de “not covered”, para let en lugar de match. Lo tienes más arriba.
  • E0308: tipos que no coinciden (mismatched types), por ejemplo cuando los brazos de un match devuelven tipos distintos ("texto" en un brazo y un número en otro).
  • E0382: borrow of partially moved value (préstamo de un valor movido en parte), cuando un brazo como Some(name) saca un String y después usas el original. Haz match sobre &nickname.
  • E0507: cannot move out of a borrowed value, cuando un brazo intenta sacar un String de algo que solo tenías prestado.
  • Todos los errores del compilador: la lista completa de páginas de errores.

Comprueba lo aprendido

1. Añades una variante Refunded a un enum. ¿Qué hace el compilador con las expresiones match que ya tienes y que nombran cada variante?

2. Haces match de un &str contra "yes" y "no". ¿Cómo arreglas E0004?

3. ¿Cuál es la desventaja de _ => en un match sobre tu propio enum?

Puntuación: 0 / 3