Errores personalizados en Rust
En la página de Result, todos los errores eran un simple String. Eso basta para ejemplos pequeños, pero se queda corto en cuanto una función puede fallar de más de una manera distinta: con un String no puedes usar match, así que quien llama no sabe qué tipo de fallo ocurrió sin ponerse a analizar el propio texto.
El problema de los errores String
Sección titulada «El problema de los errores String»fn withdraw(balance: f64, amount: f64) -> Result<f64, String> { if amount > balance { return Err(String::from("fondos insuficientes")); } if amount < 0.0 { return Err(String::from("la cantidad no puede ser negativa")); } Ok(balance - amount)}Si quien llama quiere reaccionar de forma distinta a “fondos insuficientes” y a “cantidad negativa”, la única opción que tiene es comparar el propio texto del error. Eso es frágil: se rompe en cuanto cambias la redacción de un mensaje.
Definir el error como un enum
Sección titulada «Definir el error como un enum»Un enum lo resuelve: cada tipo de fallo se convierte en su propia variante, y quien llama puede usar match directamente sobre ella:
enum WithdrawError { InsufficientFunds, NegativeAmount,}
fn withdraw(balance: f64, amount: f64) -> Result<f64, WithdrawError> { if amount < 0.0 { return Err(WithdrawError::NegativeAmount); } if amount > balance { return Err(WithdrawError::InsufficientFunds); } Ok(balance - amount)}
fn main() { match withdraw(100.0, 150.0) { Ok(remaining) => println!("Nuevo saldo: {remaining}"), Err(WithdrawError::InsufficientFunds) => println!("No tienes suficiente dinero"), Err(WithdrawError::NegativeAmount) => println!("No puedes retirar una cantidad negativa"), }}Salida:
No tienes suficiente dineroAhora el compilador se asegura de que se maneja cada tipo de fallo. Es la misma garantía que match siempre te ha dado con los enums, solo que aplicada a los errores.
Implementar Display para tener un buen mensaje de error
Sección titulada «Implementar Display para tener un buen mensaje de error»Si ahora intentas imprimir WithdrawError con {}, no compila: Rust no sabe cómo convertirlo en texto. Implementar el trait Display lo soluciona. Los traits tienen sus propias páginas un poco más adelante (Traits y Traits comunes). Por ahora, puedes copiar este patrón tal cual:
use std::fmt;
enum WithdrawError { InsufficientFunds, NegativeAmount,}
impl fmt::Display for WithdrawError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { WithdrawError::InsufficientFunds => write!(f, "fondos insuficientes"), WithdrawError::NegativeAmount => write!(f, "la cantidad no puede ser negativa"), } }}
fn main() { let error = WithdrawError::InsufficientFunds; println!("Error: {error}");}Salida:
Error: fondos insuficientesAhora WithdrawError se comporta como cualquier otro error que quieras mostrarle a un usuario o guardar en un registro (log), y sigue siendo posible usar match sobre él cuando el código necesita reaccionar a un caso concreto.
Cuando un crate puede ayudarte
Sección titulada «Cuando un crate puede ayudarte»En un proyecto real, escribir a mano Display y todo el código repetitivo de cada tipo de error se vuelve pesado. El crate (paquete de Rust) thiserror genera casi todo por ti con un #[derive], y anyhow es muy popular en aplicaciones que solo necesitan propagar errores sin definir un tipo para cada uno. Conviene saber que existen, pero no los necesitas para aprender los fundamentos. Cuando te sientas preparado, la página de anyhow y thiserror te los muestra en acción y te explica cuándo elegir cada uno.
Práctica
Sección titulada «Práctica»1. Errores de inicio de sesión
Sección titulada «1. Errores de inicio de sesión»Define un enum LoginError con dos variantes, EmptyUsername y PasswordTooShort, e implementa Display para él. Después escribe fn check_login(user: &str, password: &str) -> Result<String, LoginError>, que devuelva Ok con un mensaje de bienvenida o el error que corresponda. La contraseña debe tener al menos 8 caracteres. Pista: .is_empty() y .len() sobre un &str te dan todo lo que necesitas para comprobarlo.
Ver solución
use std::fmt;
enum LoginError { EmptyUsername, PasswordTooShort,}
impl fmt::Display for LoginError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { LoginError::EmptyUsername => write!(f, "el nombre de usuario no puede estar vacío"), LoginError::PasswordTooShort => write!(f, "la contraseña debe tener al menos 8 caracteres"), } }}
fn check_login(user: &str, password: &str) -> Result<String, LoginError> { if user.is_empty() { return Err(LoginError::EmptyUsername); } if password.len() < 8 { return Err(LoginError::PasswordTooShort); } Ok(format!("¡Te damos la bienvenida, {user}!"))}
fn main() { let attempts = [("ana", "correct-horse"), ("", "secret123"), ("ben", "short")];
for (user, password) in attempts { match check_login(user, password) { Ok(message) => println!("{message}"), Err(e) => println!("Error: {e}"), } }}Salida:
¡Te damos la bienvenida, ana!Error: el nombre de usuario no puede estar vacíoError: la contraseña debe tener al menos 8 caracteresLas contraseñas de ejemplo se quedan en inglés a propósito: .len() cuenta bytes, y una letra con tilde ocupa dos, así que cambiar el texto podría cambiar el resultado.
2. Errores que llevan detalles
Sección titulada «2. Errores que llevan detalles»Escribe fn parse_percent(input: &str) -> Result<u32, PercentError>, donde PercentError tiene dos variantes con datos: NotANumber(String) (el texto incorrecto) y TooLarge(u32) (el número que pasaba de 100). Haz que los mensajes de Display incluyan esos detalles. Pista: las variantes de un enum pueden guardar valores, como viste en la página de Enums. En fmt, usa match con PercentError::TooLarge(n) => ... para sacar el valor.
Ver solución
use std::fmt;
enum PercentError { NotANumber(String), TooLarge(u32),}
impl fmt::Display for PercentError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { PercentError::NotANumber(text) => write!(f, "'{text}' no es un número"), PercentError::TooLarge(n) => write!(f, "{n} es mayor que 100"), } }}
fn parse_percent(input: &str) -> Result<u32, PercentError> { match input.parse::<u32>() { Ok(n) => { if n > 100 { Err(PercentError::TooLarge(n)) } else { Ok(n) } } Err(_) => Err(PercentError::NotANumber(input.to_string())), }}
fn main() { for input in ["75", "mitad", "250"] { match parse_percent(input) { Ok(n) => println!("{n}%"), Err(e) => println!("Error: {e}"), } }}Salida:
75%Error: 'mitad' no es un númeroError: 250 es mayor que 100Resumen
Sección titulada «Resumen»- Un error
Stringsirve para ejemplos pequeños, pero quien llama no puede distinguir los casos conmatch. - Define un enum con una variante por cada tipo de fallo, para que quien llama pueda usar
matchsobre el error concreto. - Implementa
Display(impl fmt::Display for TuError) para poder imprimir el tipo de error con{}. - En proyectos grandes, crates como
thiserroryanyhoweliminan casi todo el código repetitivo.
Comprueba lo que aprendiste
Sección titulada «Comprueba lo que aprendiste»Comprueba lo aprendido
1. ¿Por qué un error de tipo String se queda corto cuando una función puede fallar de varias maneras?
2. ¿Qué permite a quien llama a withdraw reaccionar de forma distinta a InsufficientFunds y a NegativeAmount?
3. ¿Qué te permite hacer implementar Display en un tipo de error propio?
4. ¿En qué ayudan crates como thiserror y anyhow?
5. ¿Por qué comparar directamente el texto de los mensajes de error se considera frágil?
Puntuación: 0 / 5
Para leer más
Sección titulada «Para leer más»- Defining an error type — Rust by Example (en inglés): un ejemplo corto y ejecutable de un tipo de error propio con
Display. - Wrapping errors — Rust by Example (en inglés): el siguiente paso, un enum de error que envuelve otros errores y funciona con
?gracias aFrom. std::error::Error— documentación de std (en inglés): el trait estándar que implementan los tipos de error “de verdad”, y para qué sirve su métodosource().- ¿Hacer panic! o no hacer panic!? — El libro de Rust (en inglés): pautas para decidir cuándo devolver un error y cuándo detener el programa.
- thiserror — docs.rs (en inglés): el crate que deriva
DisplayyErrorpara tus enums de error, así escribes mucho menos código.