thiserror y anyhow en Rust: manejo de errores idiomático con las librerías más usadas

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

Criteriothiserroranyhow
Tipo de proyectoLibrería / crate públicoAplicación / binario
Matching de erroresSí, exhaustivoNo (tipo borrado)
Contexto enriquecidoMensajes en el derive.context() en cadena
OverheadCero (compile time)Mínimo (un Box extra)

Resumen

  • thiserror genera todo el boilerplate de Display, Error y From con macros derive.
  • #[from] implementa automáticamente From para 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! y ensure! acortan los retornos de error manuales.
  • Combínalos: la librería usa thiserror; la aplicación que la consume usa anyhow.

COMPARTE ESTE ARTÍCULO

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