Lección 16 de 26 Colecciones y errores
Manejo de errores: Result y ?
- Hay dos tipos de errores. Los irrecuperables son bugs: panic! detiene el programa al instante.
- Los recuperables se esperan. parse devuelve un Result: Ok(valor) si va bien o Err(error) si falla.
- Con match tratas cada caso. Un Err no rompe nada: tú decides qué hacer y el programa sigue.
- El operador ? propaga el error: con Ok saca el valor y sigue; con Err, la función termina y lo devuelve.
Los programas fallan: un fichero que no existe, un texto que no es un número, una conexión que se corta. Muchos lenguajes gestionan estos casos con excepciones que pueden saltar en cualquier momento. Rust no tiene excepciones. En su lugar, distingue dos tipos de errores y te obliga a tratarlos de forma explícita:
- Irrecuperables: algo que nunca debería pasar, como un bug. El programa se detiene con
panic!. - Recuperables: algo que puede pasar y que sabes gestionar, como un dato mal escrito. Se representan con el tipo
Result<T, E>.
panic!: errores irrecuperables
La macro panic! detiene el programa al instante y muestra tu mensaje junto al fichero y la línea donde ocurrió:
fn main() {
println!("Antes del panic");
panic!("¡Algo ha ido muy mal!");
}No siempre lo provocas tú. Algunas operaciones hacen panic solas cuando reciben algo imposible, como leer una posición que no existe en un vector:
fn main() {
let numeros = vec![1, 2, 3];
let n = numeros[99];
println!("{n}");
}¿Cuándo usar panic!? Cuando el programa llega a un estado que indica un bug y no tiene sentido seguir: un dato que siempre debería ser válido y no lo es. También es aceptable en prototipos, ejemplos y tests. Para errores que esperas que ocurran (el usuario escribe mal un número, falta un fichero) usa Result.
Result: errores recuperables
Result es un enum de la biblioteca estándar, como Option, con dos variantes:
enum Result<T, E> {
Ok(T), // todo ha ido bien: contiene el valor de tipo T
Err(E), // algo ha fallado: contiene el error de tipo E
}T y E son tipos genéricos: T es el tipo del valor y E el del error. Los verás a fondo en la próxima lección.
Un ejemplo real: convertir un texto en número con parse. Como el texto puede no ser un número, parse no devuelve un i32, sino un Result<i32, ParseIntError>. Con ::<i32> le indicas a qué tipo quieres convertir:
fn main() {
for texto in ["42", "hola"] {
match texto.parse::<i32>() {
Ok(numero) => println!("{texto} → el doble es {}", numero * 2),
Err(error) => println!("{texto} → error: {error}"),
}
}
}Igual que con Option, el compilador no te deja usar el número sin comprobar antes qué variante tienes. Es imposible olvidarte del caso de error.
Atajos: unwrap y expect
Escribir un match cada vez es largo. Cuando estás seguro de que no habrá error, o en un prototipo, tienes dos atajos:
unwrap(): devuelve el valor de dentro delOk. Si esErr, hace panic.expect("mensaje"): igual queunwrap, pero el panic muestra tu mensaje, lo que ayuda a encontrar el problema.
fn main() {
let a: i32 = "10".parse().unwrap();
println!("a = {a}");
let b: i32 = "diez".parse().expect("la configuración debe ser un número");
println!("b = {b}");
}El primer parse funciona. El segundo hace panic con la configuración debe ser un número: ParseIntError { kind: InvalidDigit }.
Consejo: Prefiere
expectaunwrap. El mensaje documenta por qué crees que nunca fallará y, si falla, sabrás exactamente dónde mirar.
Si en caso de error te vale un valor por defecto, usa unwrap_or. Nunca hace panic:
fn main() {
let edad: u32 = "treinta".parse().unwrap_or(0);
println!("Edad: {edad}");
}Transformar el error: map_err
A veces el error que recibes no es el que quieres devolver. map_err transforma el Err y deja el Ok intacto. Recibe una función corta, un closure (|e| ...), que verás a fondo más adelante:
fn parsear_puerto(texto: &str) -> Result<u16, String> {
texto
.parse::<u16>()
.map_err(|e| format!("puerto «{texto}» no válido: {e}"))
}
fn main() {
println!("{:?}", parsear_puerto("8080"));
println!("{:?}", parsear_puerto("99999"));
}El operador ?
Muchas veces una función no sabe qué hacer con un error y lo mejor es propagarlo: devolvérselo a quien la llamó para que decida. Con match queda así:
use std::num::ParseIntError;
fn sumar(a: &str, b: &str) -> Result<i32, ParseIntError> {
let x = match a.parse::<i32>() {
Ok(n) => n,
Err(e) => return Err(e),
};
let y = match b.parse::<i32>() {
Ok(n) => n,
Err(e) => return Err(e),
};
Ok(x + y)
}Este patrón es tan común que Rust tiene un atajo: el operador ?. Colocado detrás de un Result, si es Ok extrae el valor y la función sigue; si es Err, la función termina ahí y devuelve ese error.
use std::num::ParseIntError;
fn sumar(a: &str, b: &str) -> Result<i32, ParseIntError> {
let x = a.parse::<i32>()?;
let y = b.parse::<i32>()?;
Ok(x + y)
}
fn main() {
println!("{:?}", sumar("2", "40"));
println!("{:?}", sumar("2", "cuarenta"));
}Las dos versiones hacen exactamente lo mismo, pero la segunda se lee de un vistazo. Fíjate en que, al final, el resultado se envuelve en Ok(...).
Solo en funciones que devuelven Result u Option
Como ? puede salir de la función con un error, la función tiene que devolver un Result (o un Option). En un main normal, que no devuelve nada, no compila:
fn main() {
let n: i32 = "5".parse()?;
println!("{n}");
}Con Option funciona igual: si el valor es None, la función devuelve None en ese punto.
fn inicial_mayuscula(texto: &str) -> Option<char> {
let letra = texto.chars().next()?;
Some(letra.to_ascii_uppercase())
}
fn main() {
println!("{:?}", inicial_mayuscula("rust"));
println!("{:?}", inicial_mayuscula(""));
}main también puede devolver Result
Para usar ? en main, haz que devuelva un Result. El tipo más cómodo es Result<(), Box<dyn std::error::Error>>, que significa «nada si todo va bien o cualquier tipo de error». ? convierte el error concreto a ese tipo automáticamente:
use std::error::Error;
fn main() -> Result<(), Box<dyn Error>> {
let ancho: u32 = "8".parse()?;
let alto: u32 = "5".parse()?;
println!("Área: {}", ancho * alto);
Ok(())
}Si main devuelve un Err, el programa termina mostrando Error: ... y con un código de salida distinto de cero. Verás Box y dyn con calma en las lecciones de traits y smart pointers.
Tus propios errores
En tus programas, lo ideal es describir los errores posibles con un enum. Así quien llame a tu función puede reaccionar a cada caso con match. Para poder imprimirlos con {}, implementa std::fmt::Display:
use std::fmt;
enum ErrorRetirada {
CantidadCero,
SaldoInsuficiente { saldo: u32, pedido: u32 },
}
impl fmt::Display for ErrorRetirada {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
ErrorRetirada::CantidadCero => write!(f, "no puedes retirar 0 €"),
ErrorRetirada::SaldoInsuficiente { saldo, pedido } => {
write!(f, "tienes {saldo} € y quieres retirar {pedido} €")
}
}
}
}
fn retirar(saldo: u32, cantidad: u32) -> Result<u32, ErrorRetirada> {
if cantidad == 0 {
return Err(ErrorRetirada::CantidadCero);
}
if cantidad > saldo {
return Err(ErrorRetirada::SaldoInsuficiente { saldo, pedido: cantidad });
}
Ok(saldo - cantidad)
}
fn main() {
for cantidad in [30, 0, 500] {
match retirar(100, cantidad) {
Ok(restante) => println!("Retiras {cantidad} €. Te quedan {restante} €"),
Err(e) => println!("Error: {e}"),
}
}
}write! funciona como format!, pero escribe en el formateador f. Por ahora copia este patrón tal cual: en la lección de traits verás qué significa impl ... for.
Para ir más allá: Si además añades
#[derive(Debug)]al enum eimpl std::error::Error for ErrorRetirada {}, tu error se podrá propagar con?desde unmainque devuelveBox<dyn Error>.
¿panic o Result?
| Situación | Qué usar |
|---|---|
| Un bug: algo que nunca debería ocurrir | panic! |
| Prototipos, ejemplos y tests | unwrap / expect |
| Un error esperable que alguien puede gestionar | Result con match |
| Un error con un valor por defecto razonable | unwrap_or |
| No sabes gestionarlo aquí, pero quien te llama sí | ? |
Hora de practicar
Ejercicios
0/5 resueltos
Reto 1 de 5 Resuelto
entradas y usa un match sobre texto.parse::<i32>(). Si se puede convertir, imprime Número: N; si no, imprime Error: TEXTO no es un número (por ejemplo, Error: hola no es un número).Número: 42 Error: hola no es un número Número: -7
Al ejecutar, unos tests ocultos comprueban tus funciones.
Pulsa Ejecutar para compilar y ver aquí la salida de tu programa.