Ir al contenido

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.

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.

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 dinero
▶ Pruébalo tú mismo

Ahora 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 insuficientes
▶ Pruébalo tú mismo

Ahora 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.

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.

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ío
Error: la contraseña debe tener al menos 8 caracteres
▶ Pruébalo tú mismo

Las 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.

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úmero
Error: 250 es mayor que 100
▶ Pruébalo tú mismo
  • Un error String sirve para ejemplos pequeños, pero quien llama no puede distinguir los casos con match.
  • Define un enum con una variante por cada tipo de fallo, para que quien llama pueda usar match sobre el error concreto.
  • Implementa Display (impl fmt::Display for TuError) para poder imprimir el tipo de error con {}.
  • En proyectos grandes, crates como thiserror y anyhow eliminan casi todo el código repetitivo.

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