panic! y Result son dos mecanismos para representar fallos irrecuperables o gestionables.
En muchos lenguajes modernos, cuando algo va mal se lanza una excepción. El flujo salta hasta encontrar un bloque catch y, si nadie la captura, termina propagándose hasta el runtime.
Las excepciones no comprobadas no siempre aparecen en la firma. Mirando una función como public void procesar(), puede ser difícil saber qué fallos propaga sin revisar su implementación o documentación.
Rust toma un camino diferente. Divide los errores en dos categorías fundamentales:
- Pánicos (
panic!): El código ha llegado a un estado en el que no puede continuar de forma razonable, a menudo por un bug o una precondición incumplida. - Errores recuperables (
Result<T, E>): Situaciones esperadas (archivo no encontrado, fallo de red) que el llamador puede gestionar.
Errores irrecuperables: panic!
Un pánico ocurre cuando el código detecta un bug. Por ejemplo:
- Intentar acceder al índice 10 de un array de 5 elementos.
- Dividir un entero por cero.
- Llamar explícitamente a la macro
panic!().
Cuando ocurre un pánico, la configuración habitual inicia un proceso llamado unwinding (desbobinado). Rust recorre la pila hacia atrás y destruye los valores de cada marco antes de terminar el hilo que ha entrado en pánico. También puede configurarse panic = "abort" para abortar sin desbobinar.
fn main() {
// Esto es un pánico explícito
panic!("¡Algo terrible ha sucedido!");
}Al ejecutarlo, verás:
thread 'main' panicked at '¡Algo terrible ha sucedido!', src/main.rs:2:5Backtrace
Si quieres saber exactamente qué llamadas llevaron al error, puedes ejecutar tu programa con una variable de entorno especial:
RUST_BACKTRACE=1 cargo run
¿Cuándo usar panic!?
Solo cuando el programa está roto.
- No uses
panic!si el usuario introduce mal una contraseña. - Puede tener sentido si el código detecta que una configuración interna imprescindible falta o es incoherente y no existe una forma razonable de continuar.
Errores recuperables: Result<T, E>
La mayoría de errores no son bugs, son circunstancias de la vida real: un servidor caído, un disco lleno, un formato de fecha incorrecto. No queremos matar el programa; queremos avisar al usuario o reintentar.
Para esto, Rust usa el Enum Result:
enum Result<T, E> {
Ok(T), // La operación fue bien y contiene el valor T
Err(E), // La operación falló y contiene el error E
}Fíjate que es muy parecido a Option. Pero mientras Option es “algo o nada”, Result es “éxito o fallo”.
El ejemplo clásico: Abrir un archivo
La función File::open no devuelve un File. Devuelve un Result<File, Error>.
use std::fs::File;
fn main() {
let f = File::open("hola.txt"); // f es de tipo Result<File, std::io::Error>
let archivo = match f {
Ok(file) => file,
Err(error) => {
panic!("Hubo un problema al abrir el archivo: {:?}", error);
},
};
}No puedes usar el archivo como si File::open devolviera directamente un File. Antes necesitas manejar, transformar o propagar el Result.
Atajos para pánicos: unwrap y expect
A veces escribir un match es demasiado verboso, especialmente si estás haciendo prototipos o tests y no te importa que el programa explote si algo falla.
Rust ofrece métodos en Result que extraen el valor si es Ok o entran en pánico si es Err.
unwrap()
“Dame el valor o muere”.
let f = File::open("hola.txt").unwrap();Si el archivo existe, f será el File. Si no, el programa hará panic!.
expect(msg)
“Dame el valor o muere con este mensaje”.
let f = File::open("hola.txt").expect("Fallo al abrir hola.txt");Es igual que unwrap, pero te permite personalizar el mensaje de error del pánico. Es mucho mejor para depurar.
Buenas prácticas:
En rutas que pueden fallar de forma esperada, evita usar unwrap(). Usa expect() si el fallo representa una invariante rota y quieres explicar el motivo, o devuelve y gestiona el error cuando el llamador pueda recuperarse.
Propagar errores con ?
A menudo, no queremos manejar el error en la función actual, sino pasárselo a la función que nos llamó para que ella decida qué hacer.
En otros lenguajes harías un throw (lanzar excepción). En Rust, devolvemos el error.
Forma larga (con match):
use std::fs::File;
use std::io::{self, Read};
fn leer_usuario() -> Result<String, io::Error> {
let f = File::open("usuario.txt");
let mut f = match f {
Ok(file) => file,
Err(e) => return Err(e), // <--- Devolvemos el error antes de tiempo
};
let mut s = String::new();
match f.read_to_string(&mut s) {
Ok(_) => Ok(s),
Err(e) => Err(e), // <--- Devolvemos el error
}
}Esto es tedioso. Rust tiene un operador muy cómodo para esto: el signo de interrogación ?.
Forma corta (con ?):
use std::fs::File;
use std::io::{self, Read};
fn leer_usuario() -> Result<String, io::Error> {
// Si falla, devuelve Err(e) inmediatamente. Si va bien, nos da el File.
let mut f = File::open("usuario.txt")?;
let mut s = String::new();
f.read_to_string(&mut s)?; // Si falla devuelve Err; si no, continúa
Ok(s) // Si llegamos aquí, todo fue bien
}El operador ? hace tres cosas:
- Mira el
Result. - Si es
Ok, extrae el valor y continúa. - Si es
Err, sale de la función y propaga el error, convirtiéndolo cuando el tipo de retorno lo permite medianteFrom.
Incluso podemos encadenarlo:
fn leer_usuario_corto() -> Result<String, io::Error> {
let mut s = String::new();
File::open("usuario.txt")?.read_to_string(&mut s)?;
Ok(s)
}Resumen
El manejo de errores en Rust es explícito y seguro.
panic!: Para errores irrecuperables (bugs). El programa se detiene.Result<T, E>: Para errores recuperables. Debes manejarlos.unwrap()/expect(): Extraen el valor o causan pánico. Úsalos con precaución.- Operador
?: Propaga el error hacia arriba y puede convertir su tipo.
Ahora que sabemos propagar errores, surge una duda: ¿Qué pasa si una función devuelve un error de base de datos y otra un error de lectura de archivo? ¿Cómo los devuelvo en la misma función?
En el próximo artículo veremos Option, el tipo que representa la ausencia esperada de un valor. Después volveremos a Result para profundizar en sus combinadores y en los tipos de error.