Manejo de Errores: Option, Result, ?, panic!
Option maneja valores que pueden estar ausentes, Result maneja operaciones que pueden fallar, y el operador ? te deja propagar cualquiera con limpieza.
Rust divide 'las cosas que pueden salir mal' en dos categorías deliberadamente distintas, y confundirlas es uno de los errores más comunes entre quienes recién llegan al lenguaje. Option<T> modela la ausencia de un valor — un valor que simplemente no está ahí, sin ninguna noción de culpa asociada: buscar una clave que nunca se insertó, o encontrar el primer número par en una lista que resulta no tener ninguno. Result<T, E> modela una operación que puede fallar, y lleva consigo una razón de por qué: analizar un texto que no es un número válido, abrir un archivo que no existe, o hacer una petición de red que agota el tiempo de espera. Ninguno de los dos es un error de programación — son resultados esperados y cotidianos para los que tu programa debe estar preparado — y Rust te obliga a prepararte para ellos haciendo que ambos tipos sean imposibles de ignorar en silencio.
Option<T> tiene exactamente dos variantes: Some(valor) cuando algo está presente, y None cuando no lo está — por debajo, no es más que un enum, y por eso todo lo que aprendiste sobre match en la lección anterior se aplica directamente aquí. Una función como find_first_even(&[i32]) -> Option<i32> devuelve Some(n) en cuanto encuentra una coincidencia y cae en None si el bucle nunca la encuentra; quien la llama está entonces obligado a manejar ambas ramas, ya sea con un match completo, un if let, o alguno de los muchos métodos combinadores de Option como unwrap_or(valor_por_defecto), que ofrece un valor de respaldo en lugar de arriesgarse jamás a un crash. Compara esto con lenguajes que representan 'ningún valor' como null: ahí, toda referencia es secretamente nula y el sistema de tipos no te ayuda en nada a recordar que debes comprobarlo, que es exactamente el defecto que Tony Hoare bautizó como su 'error de los mil millones de dólares.' En Rust, un i32 simple jamás puede estar ausente — solo un Option<i32> puede estarlo — así que es el propio tipo el que te avisa, a ti y al compilador, de cuándo hace falta una comprobación.
Result<T, E> es el hermano de Option para operaciones que fallan con una razón: Ok(valor) en caso de éxito, Err(error) en caso de fallo, donde E es el tipo de error que tenga sentido para esa operación — input.parse::<i32>(), por ejemplo, devuelve Result<i32, ParseIntError>, porque un análisis fallido realmente tiene algo útil que decir sobre qué salió mal. Igual que con Option, un Result que recibes no se puede descartar en silencio y tratar como un éxito por accidente: el compilador emite una advertencia unused_must_use si lo ignoras por completo, y usar el valor de la rama equivocada (por ejemplo, desempaquetar un Err como si fuera Ok) simplemente no pasa la verificación de tipos.
Manejar cada Result con un match explícito se vuelve ruidoso rápidamente en una función que encadena varias llamadas que pueden fallar, y ese es exactamente el problema que resuelve el operador ?. Colocado después de una expresión que produce un Result, ? desempaqueta el valor si es Ok y deja que la ejecución continúe con normalidad, o — si es Err — retorna inmediatamente ese error desde la función que lo contiene, convirtiéndolo si hace falta mediante el trait From. Esto significa que ? solo se puede usar dentro de una función cuyo propio tipo de retorno sea Result (o Option, que admite el mismo operador) con un tipo de error compatible; la recompensa es que una cadena de pasos que pueden fallar se lee casi como si el camino feliz fuera el único camino, con cada salida temprana resuelta con un solo carácter en lugar de un bloque match por llamada.
unwrap() y expect("mensaje") son las válvulas de escape tanto de Option como de Result: extraen directamente el valor de Some/Ok, y entran en pánico de inmediato si lo que encuentran es None/Err en su lugar — expect al menos te deja adjuntar un mensaje explicando qué esperabas y por qué. Recurrir a unwrap() en código de aplicación que vas a desplegar es una señal de alarma real: convierte un fallo recuperable (un archivo que podría no existir, una entrada que podría estar malformada) en un crash irrecuperable, sin que quien llama tenga oportunidad de reintentar, registrar el error o degradar el servicio con elegancia. Es perfectamente aceptable en prototipos rápidos, scripts desechables y pruebas, y expect con un mensaje está bien en cualquier lugar donde puedas demostrar que el caso de fallo es realmente imposible — el mensaje se convierte en la documentación de esa prueba, y la siguiente persona que toque el código (incluido tú mismo en el futuro) te lo agradecerá por haber dejado escrito por qué estabas tan seguro.
panic! es la herramienta de Rust para el otro tipo de fallo, completamente distinto: no 'esto podría no funcionar', sino 'esto no debe pasar jamás, y si pasa, algo está roto de una forma que ningún manejo de errores más adelante puede arreglar.' Una función que recibe argumentos que violan una precondición documentada — dividir entre un divisor que su propio contrato dice que nunca puede ser cero — está justificada al entrar en pánico, porque seguir ejecutándose más allá de un invariante roto arriesga corromper datos o producir respuestas silenciosamente incorrectas, lo cual es peor que detenerse haciendo ruido. La línea divisoria es la intención: usa Result para todo lo que quien llama pueda razonablemente esperar que ocurra y quiera poder reaccionar ante ello — una entrada de usuario incorrecta, un tropiezo de red, un archivo faltante — y reserva panic! (y por extensión unwrap/expect) para suposiciones violadas y errores de programación genuinos que nadie que llame debería estar en posición de 'manejar' en primer lugar.
Sentirte cómodo con esta división — Option para la ausencia, Result para el fallo esperado con una razón, ? para propagar cualquiera de los dos hacia arriba en la pila de llamadas, y panic! reservado para invariantes rotos — es lo que permite que el código Rust sea, al mismo tiempo, directo sobre lo que puede salir mal y agradable de leer, porque es el compilador, y no una comprobación de null en tiempo de ejecución ni una excepción sin manejar tres marcos de pila más arriba, quien te obliga a ser honesto.
fn find_first_even(numbers: &[i32]) -> Option<i32> {for &n in numbers {if n % 2 == 0 {return Some(n);}}None}fn main() {let numbers = [1, 3, 5, 8, 9];match find_first_even(&numbers) {Some(n) => println!("First even number: {}", n),None => println!("No even number found"),}// unwrap_or gives a fallback instead of panicking on None.let value = find_first_even(&[1, 3, 5]).unwrap_or(-1);println!("value = {}", value);}
Option<T> models an absent value with no reason attached — searching a slice can come back Some or None, and unwrap_or supplies a fallback instead of risking a crash.
use std::num::ParseIntError;fn parse_and_double(input: &str) -> Result<i32, ParseIntError> {let n: i32 = input.parse()?; // ? returns early with Err if parsing failsOk(n * 2)}fn main() {match parse_and_double("21") {Ok(n) => println!("Doubled: {}", n),Err(e) => println!("Failed to parse: {}", e),}match parse_and_double("not a number") {Ok(n) => println!("Doubled: {}", n),Err(e) => println!("Failed to parse: {}", e),}}
The ? operator propagates a Result's Err straight out of the enclosing function, letting a fallible parse-and-transform read like a single straight-line computation.
struct Config {max_connections: u32,}impl Config {fn from_env(value: &str) -> Config {let max_connections = value.parse().expect("MAX_CONNECTIONS must be a valid u32 — this is a startup bug, not user input");Config { max_connections }}}fn divide(a: f64, b: f64) -> f64 {if b == 0.0 {// An unrecoverable programming error: the caller broke a precondition.panic!("divide called with b = 0.0, which is never a valid input");}a / b}fn main() {let config = Config::from_env("100");println!("max_connections = {}", config.max_connections);println!("10 / 2 = {}", divide(10.0, 2.0));}
expect() documents a proven assumption at startup, while an explicit panic! marks a genuinely unrecoverable precondition violation — contrast both with the ordinary, successful path they don't interrupt here.
🧠 Comprueba tu comprensión
0/1 · 0/1 answered1. A function calls .unwrap() on a Result instead of propagating it with ?. What's the practical difference for the code that calls this function?