El manejo de errores en Rust con Result<T, E> y el operador ? es ergonómico, pero definir los tipos de error manualmente resulta tedioso. Las bibliotecas thiserror y anyhow son las dos soluciones de facto: thiserror para librerías que exponen errores tipados, anyhow para aplicaciones y binarios que priorizan la propagación con contexto sobre la tipificación exhaustiva.
thiserror: errores tipados para librerías
thiserror genera implementaciones de std::error::Error, Display y From a partir de un derive macro. El resultado es un enum de error limpio y bien documentado que los usuarios de tu librería pueden inspeccionar y manejar de forma exhaustiva.
// [dependencies]
// thiserror = "2"
use thiserror::Error;
use std::path::PathBuf;
#[derive(Error, Debug)]
pub enum ErrorApp {
#[error("archivo no encontrado: {0}")]
ArchivoNoEncontrado(PathBuf),
#[error("error de parseo en línea {linea}: {mensaje}")]
ErrorParseo { linea: usize, mensaje: String },
#[error("valor fuera de rango: {valor} no está en [{min}, {max}]")]
ValorFueraDeRango { valor: f64, min: f64, max: f64 },
#[error("error de E/S")]
Io(#[from] std::io::Error),
#[error("error de red")]
Red(#[source] Box<dyn std::error::Error + Send + Sync>),
}
fn leer_config(path: &std::path::Path) -> Result<String, ErrorApp> {
std::fs::read_to_string(path)
.map_err(|e| if e.kind() == std::io::ErrorKind::NotFound {
ErrorApp::ArchivoNoEncontrado(path.to_owned())
} else {
ErrorApp::Io(e)
})
}
fn parsear_porcentaje(s: &str, linea: usize) -> Result<f64, ErrorApp> {
let v: f64 = s.trim().parse().map_err(|_| ErrorApp::ErrorParseo {
linea,
mensaje: format!("'{s}' no es un número"),
})?;
if v < 0.0 || v > 100.0 {
return Err(ErrorApp::ValorFueraDeRango { valor: v, min: 0.0, max: 100.0 });
}
Ok(v)
}
anyhow: propagación con contexto para aplicaciones
anyhow simplifica el manejo de errores en binarios y aplicaciones donde no necesitas distinguir exhaustivamente los tipos de error, pero sí quieres añadir contexto descriptivo en cada punto de la cadena de propagación.
// [dependencies]
// anyhow = "1"
// thiserror = "2"
use anyhow::{Context, Result, bail, ensure, anyhow};
fn leer_numero(path: &str) -> Result<i64> {
let contenido = std::fs::read_to_string(path)
.with_context(|| format!("no se pudo leer '{path}'"))?;
let numero: i64 = contenido.trim().parse()
.with_context(|| format!("'{path}' no contiene un entero válido"))?;
ensure!(numero > 0, "el número debe ser positivo, encontrado: {numero}");
Ok(numero)
}
fn procesar(path: &str) -> Result<String> {
let n = leer_numero(path)
.context("al procesar el archivo de entrada")?;
if n > 1_000_000 {
bail!("número demasiado grande para procesar: {n}");
}
Ok(format!("raíz cuadrada de {n}: {:.4}", (n as f64).sqrt()))
}
fn main() -> Result<()> {
match procesar("entrada.txt") {
Ok(resultado) => println!("{resultado}"),
Err(e) => {
eprintln!("Error: {e}");
// anyhow mantiene la cadena de causas
for causa in e.chain().skip(1) {
eprintln!(" causado por: {causa}");
}
std::process::exit(1);
}
}
Ok(())
}
Combinar thiserror en librerías y anyhow en binarios
El patrón recomendado es usar thiserror en los crates de librería (para que los errores sean inspeccionables por los llamadores) y convertir esos errores a anyhow::Error en el binario o la capa de aplicación, donde solo importa propagar con contexto.
// En la librería (usa thiserror):
use thiserror::Error;
#[derive(Error, Debug)]
pub enum ErrorConexion {
#[error("timeout después de {segundos}s")]
Timeout { segundos: u64 },
#[error("host no alcanzable: {host}")]
HostNoAlcanzable { host: String },
#[error("autenticación fallida")]
AuthFallida,
}
pub fn conectar(host: &str) -> Result<String, ErrorConexion> {
if host.is_empty() {
return Err(ErrorConexion::HostNoAlcanzable { host: host.into() });
}
Ok(format!("Conectado a {host}"))
}
// En el binario (usa anyhow):
use anyhow::{Context, Result};
fn main() -> Result<()> {
// La conversión de ErrorConexion a anyhow::Error es automática
let conn = conectar("db.ejemplo.com")
.context("al inicializar la conexión de base de datos")?;
println!("{conn}");
Ok(())
}
Errores tipados vs contexto: cuándo usar cada uno
- Usa thiserror cuando publiques una librería cuyos errores deban ser manejables por el llamador (match exhaustivo, lógica de reintento según el tipo de error).
- Usa anyhow en binarios, servidores web y scripts donde el objetivo es loguear el error con contexto y decidir si reintentar o abortar, sin inspeccionar el tipo exacto.
- No uses Box<dyn Error> directamente en código nuevo: anyhow ofrece las mismas ventajas con API más ergonómica y cadenas de causas automáticas.
La combinación de las dos bibliotecas cubre el 95 % de los casos de manejo de errores en proyectos Rust reales sin necesidad de código boilerplate adicional.
