El manejo de errores idiomático en Rust gira alrededor de Result<T, E>. Para proyectos pequeños, implementar el trait Error a mano es suficiente. Pero cuando la base de código crece, dos librerías se han convertido en el estándar de facto: thiserror para crear tipos de error con semántica clara en librerías, y anyhow para propagar errores de forma eficiente en aplicaciones.
El problema sin librerías
use std::fmt;
#[derive(Debug)]
enum MiError {
IoError(std::io::Error),
ParseError(std::num::ParseIntError),
Custom(String),
}
impl fmt::Display for MiError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
MiError::IoError(e) => write!(f, "Error de IO: {e}"),
MiError::ParseError(e) => write!(f, "Error de parseo: {e}"),
MiError::Custom(msg) => write!(f, "{msg}"),
}
}
}
impl std::error::Error for MiError {}
impl From<std::io::Error> for MiError {
fn from(e: std::io::Error) -> Self { MiError::IoError(e) }
}
// Todo esto es boilerplate que thiserror elimina
thiserror: tipos de error tipados sin boilerplate
Añade en Cargo.toml: thiserror = "1"
use thiserror::Error;
#[derive(Debug, Error)]
enum AppError {
#[error("error de IO: {0}")]
Io(#[from] std::io::Error),
#[error("no se pudo parsear el número: {0}")]
Parse(#[from] std::num::ParseIntError),
#[error("configuración inválida: {campo} = {valor}")]
ConfigInvalida { campo: String, valor: String },
#[error("timeout después de {segundos}s")]
Timeout { segundos: u64 },
}
fn leer_config(ruta: &str) -> Result<i32, AppError> {
let contenido = std::fs::read_to_string(ruta)?; // convierte IoError automáticamente
let valor: i32 = contenido.trim().parse()?; // convierte ParseIntError
Ok(valor)
}
fn main() {
match leer_config("config.txt") {
Ok(v) => println!("Valor: {v}"),
Err(AppError::Io(e)) => println!("Fichero no encontrado: {e}"),
Err(AppError::Parse(e)) => println!("Formato incorrecto: {e}"),
Err(e) => println!("Otro error: {e}"),
}
}
anyhow: propagación sin definir tipos de error
Añade en Cargo.toml: anyhow = "1"
use anyhow::{Context, Result, bail, ensure, anyhow};
fn procesar_fichero(ruta: &str) -> Result<String> {
let contenido = std::fs::read_to_string(ruta)
.with_context(|| format!("No se pudo leer '{ruta}'"))?;
ensure!(!contenido.is_empty(), "El fichero está vacío: {ruta}");
Ok(contenido.to_uppercase())
}
fn validar_edad(edad: i32) -> Result<()> {
if edad < 0 {
bail!("La edad no puede ser negativa: {edad}");
}
if edad > 150 {
return Err(anyhow!("Edad imposible: {edad}"));
}
Ok(())
}
fn main() -> Result<()> {
validar_edad(25)?;
let texto = procesar_fichero("datos.txt")?;
println!("{texto}");
Ok(())
}
Contexto en cadena con anyhow
use anyhow::{Context, Result};
fn paso_uno() -> Result<()> {
std::fs::read_to_string("inexistente.txt")?;
Ok(())
}
fn paso_dos() -> Result<()> {
paso_uno().context("fallo en paso_uno")?;
Ok(())
}
fn main() -> Result<()> {
paso_dos().context("fallo en paso_dos")?;
Ok(())
}
// Error en pantalla con RUST_BACKTRACE=1:
// Error: fallo en paso_dos
// Caused by:
// 0: fallo en paso_uno
// 1: No such file or directory (os error 2)
Cuándo usar cada una
| Criterio | thiserror | anyhow |
|---|---|---|
| Tipo de proyecto | Librería / crate público | Aplicación / binario |
| Matching de errores | Sí, exhaustivo | No (tipo borrado) |
| Contexto enriquecido | Mensajes en el derive | .context() en cadena |
| Overhead | Cero (compile time) | Mínimo (un Box extra) |
Resumen
thiserrorgenera todo el boilerplate deDisplay,ErroryFromcon macros derive.#[from]implementa automáticamenteFrompara que?convierta el error subyacente.anyhow::Result<T>acepta cualquier error; ideal cuando no necesitas distinguir variantes..context()y.with_context()añaden mensaje sin perder la causa original.bail!yensure!acortan los retornos de error manuales.- Combínalos: la librería usa
thiserror; la aplicación que la consume usaanyhow.
