thiserror y anyhow en Rust: manejo de errores ergonómico para librerías y aplicaciones

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.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP