anyhow y thiserror en Rust: manejo de errores moderno sin boilerplate

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::Display según el atributo #[error("...")].
  • Las implementaciones de From para 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 a return Err(anyhow!(...));.
  • ensure!(cond, msg): equivalente a if !cond { bail!(msg) }.

Cuándo usar cada una

ContextoLibrería recomendada
Librería pública o interna con API tipadathiserror
Aplicación CLI, servidor, herramientaanyhow
Quieres que el llamador distinga el tipo de errorthiserror
Solo necesitas propagar y mostrar el erroranyhow
Combinado en el mismo proyectothiserror 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! y ensure! 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.

COMPARTE ESTE ARTÍCULO

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