Implementar Display, Error y From a mano para cada tipo de error es correcto pero repetitivo. La comunidad Rust ha convergido en dos librerías que resuelven esto: thiserror para librerías con tipos de error bien definidos, y anyhow para aplicaciones donde importa propagar el error más que su tipo exacto.
thiserror: errores de librería sin boilerplate
# Cargo.toml
[dependencies]
thiserror = "2"
use thiserror::Error;
#[derive(Error, Debug)]
enum ErrorApp {
#[error("Error de I/O: {0}")]
Io(#[from] std::io::Error),
#[error("Error al parsear número: {0}")]
ParseNumero(#[from] std::num::ParseIntError),
#[error("Valor inválido: {mensaje}")]
ValorInvalido { mensaje: String },
#[error("No encontrado: {0}")]
NoEncontrado(String),
}
El macro #[derive(Error)] genera automáticamente:
- La implementación de
std::error::Error. - La implementación de
fmt::Displaysegún el atributo#[error("...")]. - Las implementaciones de
Frompara los campos marcados con#[from]. - El método
source()para los campos#[from]y#[source].
fn leer_numero(ruta: &str) -> Result<i32, ErrorApp> {
let contenido = std::fs::read_to_string(ruta)?; // Io automático
let n: i32 = contenido.trim().parse()?; // ParseNumero automático
Ok(n)
}
Atributos de thiserror
#[derive(Error, Debug)]
enum MiError {
// {0} es el primer campo posicional
#[error("Error de red: {0}")]
Red(String),
// {campo} es el campo con nombre
#[error("Código {codigo}: {descripcion}")]
Http { codigo: u16, descripcion: String },
// #[from] genera From e impl source()
#[error("Error IO: {0}")]
Io(#[from] std::io::Error),
// #[source] solo impl source(), sin From
#[error("Error de base de datos")]
Db(#[source] Box<dyn std::error::Error + Send + Sync>),
}
anyhow: propagación de errores en aplicaciones
[dependencies]
anyhow = "1"
use anyhow::{Context, Result};
fn leer_config(ruta: &str) -> Result<String> {
std::fs::read_to_string(ruta)
.with_context(|| format!("No se pudo leer el fichero '{}'", ruta))
}
fn main() -> Result<()> {
let config = leer_config("config.toml")?;
println!("{}", config);
Ok(())
}
anyhow::Result<T> es un alias para Result<T, anyhow::Error>. anyhow::Error puede envolver cualquier error que implemente std::error::Error.
Context: añadir contexto al error
use anyhow::{bail, ensure, Context, Result};
fn procesar_fichero(ruta: &str) -> Result<()> {
let contenido = std::fs::read_to_string(ruta)
.context("Error leyendo el fichero de entrada")?;
let n: i32 = contenido.trim().parse()
.with_context(|| format!("El contenido '{}' no es un entero", contenido.trim()))?;
ensure!(n > 0, "El número debe ser positivo, se recibió {}", n);
Ok(())
}
.context("..."): añade un mensaje estático..with_context(|| ...): mensaje dinámico (closure, solo se evalúa si hay error).bail!(...): equivalente areturn Err(anyhow!(...));.ensure!(cond, msg): equivalente aif !cond { bail!(msg) }.
Cuándo usar cada una
| Contexto | Librería recomendada |
|---|---|
| Librería pública o interna con API tipada | thiserror |
| Aplicación CLI, servidor, herramienta | anyhow |
| Quieres que el llamador distinga el tipo de error | thiserror |
| Solo necesitas propagar y mostrar el error | anyhow |
| Combinado en el mismo proyecto | thiserror en capas internas, anyhow en main/handler |
Resumen
thiserror: genera el boilerplate de tipos de error con#[derive(Error)]. Ideal para librerías.anyhow: tipo de error genérico con contexto acumulable. Ideal para aplicaciones..context()y.with_context()añaden información para el diagnóstico.bail!yensure!simplifican la creación y verificación de condiciones.
El siguiente artículo introduce los generics: cómo escribir funciones y structs que funcionan con cualquier tipo sin duplicar código.
