Guía completa de manejo de errores en Rust para desarrolladores

rust Guía completa de manejo de errores en Rust para desarrolladores

Guía completa de manejo de errores en Rust para desarrolladores

Rust tiene un enfoque explícito y seguro para errores: Result, Option y panics. Esta guía muestra patrones idiomáticos, bibliotecas útiles y ejemplos prácticos (con estructura de proyecto y código completo) para que manejes errores correctamente tanto en bibliotecas como en aplicaciones.

Conceptos clave

  • Result<T, E>: para operaciones que pueden fallar.
  • Option<T>: para valores opcionales.
  • panic!: para errores irrecuperables (invariante rota).
  • ? operator: propagación concisa de errores.
  • Boundary: manejar errores en los límites (CLI, red, GUI), propagar dentro de librerías.

Reglas prácticas

  • En librerías: define tipos de error explícitos y documentalos.
  • En aplicaciones: usa anyhow/eyre para ergonomía.
  • No uses unwrap() ni expect() en producción salvo que sea verdaderamente inmutable.
  • Añade contexto a los errores (mensajes útiles, campo source cuando aplique).
  • No expones información sensible en mensajes de error que puedan llegar a usuarios o logs públicos.

Estructura de ejemplo del proyecto

error_handling_demo/
├─ Cargo.toml
└─ src/
   ├─ main.rs       (aplicación: usa anyhow para manejo global)
   ├─ lib.rs        (lógica: define funciones que retornan Result)
   └─ errors.rs     (tipos de error de librería con thiserror)

Cargo.toml (partes relevantes)

[package]
name = "error_handling_demo"
version = "0.1.0"
edition = "2021"

[dependencies]
thiserror = "1"
anyhow = "1"
serde = { version = "1", features = ["derive"] }
reqwest = { version = "0.11", features = ["json"] }

errors.rs — tipado de errores para una librería

En librerías es preferible errores concretos: implementa std::error::Error (thiserror facilita esto).

use thiserror::Error;

#[derive(Error, Debug)]
pub enum MyLibError {
    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),

    #[error("HTTP error: {0}")]
    Http(#[from] reqwest::Error),

    #[error("Invalid data: {0}")]
    InvalidData(String),
}

lib.rs — lógica que propaga errores

mod errors;
use errors::MyLibError;
use reqwest::blocking::Client;

pub fn fetch_and_parse(url: &str) -> Result {
    let client = Client::new();
    let resp = client.get(url).send()?; // reqwest::Error -> MyLibError via From

    let status = resp.status();
    if !status.is_success() {
        return Err(MyLibError::InvalidData(format!("status {}", status)));
    }

    let body = resp.text()?; // propagación segura
    // parse: si falla, devolver un InvalidData
    if body.trim().is_empty() {
        return Err(MyLibError::InvalidData("empty response".into()));
    }

    Ok(body)
}

Por qué así: la librería exporta un enum de errores que permite al consumidor hacer match y reaccionar. Usar #[from] evita boilerplate para conversiones.

main.rs — aplicación: contextos, logs y anyhow

use anyhow::{Context, Result};
use error_handling_demo::fetch_and_parse;

fn main() -> Result<()> {
    let url = std::env::args().nth(1).unwrap_or_else(|| "https://example.com".into());

    // Añade contexto cuando llamas a funciones que pueden fallar
    let body = fetch_and_parse(&url)
        .with_context(|| format!("failed to fetch and parse url: {}", url))?;

    println!("fetched {} bytes", body.len());

    Ok(())
}

Por qué anyhow aquí: simplifica el código de la aplicación; te da stack-trace/chain y contexto. En el 'boundary' (main) recoges, enriqueces y decides cómo responder (log + exit code, UI message, etc.).

Patrones y técnicas avanzadas

  • Agregar contexto: usar anyhow::Context o map_err(|e| MyError::...) para explicar el qué y el por qué.
  • Transformar Option a Result: option.ok_or(MyError::...)?.
  • Propagación: usa ? temprano. Evita unwrap en producción.
  • Errores de alto volumen: en hot paths, evita crear cadenas de errores con allocations frecuentes. Considera códigos de error simples (enum pequeño) o señalizar mediante Result<T, ErrorCode> y convertir solo en boundary.
  • Logs: loggea antes de descartar datos útiles para debugging, pero filtra información sensible.
  • Panics: reserva para fallos invariantes; en librerías documenta cuándo puede panicar.

Manejo de errores en async y servidores HTTP (ejemplo con axum)

// handler.rs (esqueleto)
use axum::{response::IntoResponse, http::StatusCode};
use thiserror::Error;

#[derive(Error, Debug)]
pub enum ApiError {
    #[error("Not found: {0}")]
    NotFound(String),

    #[error("Internal: {0}")]
    Internal(String),
}

impl IntoResponse for ApiError {
    fn into_response(self) -> axum::response::Response {
        let (status, body) = match &self {
            ApiError::NotFound(_) => (StatusCode::NOT_FOUND, self.to_string()),
            ApiError::Internal(_) => (StatusCode::INTERNAL_SERVER_ERROR, "internal error".into()),
        };
        (status, body).into_response()
    }
}

// usage in handler
async fn handler() -> Result {
    // ...
    Err(ApiError::NotFound("user 123".into()))
}

Por qué: convierte errores a respuestas HTTP en un solo lugar. Evitar exponer detalles internos para seguridad.

Errores comunes y cómo evitarlos

  • Usar String como error genérico en librerías —> define tipos concretos.
  • Swallowing errors: capturar y no loggear ni propagar —> pierde contexto.
  • Exponer secretos en mensajes de error —> sanitize los mensajes antes de enviarlos al cliente o logs públicos.
  • Capturar backtraces en todos los errores —> overhead; captura sólo cuando sea necesario.

Consejos de rendimiento

  • Evita cadenas largas de errores en hot loops; usa códigos/enum compactos.
  • Prefiere conversiones zero-cost (From impls) y el operador ? en vez de map_err con closures que allocan.
  • Si necesitas backtrace solo en investigación, habilítalo con variable de entorno o feature flag.

Diagnóstico y observabilidad

Integra logs estructurados, métricas y trazas de errores (Sentry, OpenTelemetry). Envuelve errores con contexto útil (ruta, IDs de petición) para correlación en trazas.

Patrón recomendado: boundary-driven

Diseña así:

  • Librería: errores tipados y detallados (enum + source).
  • Core: propaga errores internamente; añade contexto en puntos significativos.
  • Aplicación/Boundary: convertir a errores legibles por humanos / códigos HTTP / exit codes y registrar métricas.

Checklist rápido antes de lanzar

  • ¿Se están propagando errores con suficiente contexto?
  • ¿No se exponen secretos en logs/respuestas?
  • ¿Las librerías usan tipos de error estables y documentados?
  • ¿Se evita unwrap/expect en producción?
  • ¿Hay pruebas para casos de error (unit + integration)?

Siguiente paso: convierte una función crítica de tu código a seguir este patrón (define error enum, cambia retornos a Result, añade contexto en boundary) y añade una prueba que simule la falla. Si quieres, te puedo generar la PR completa con los cambios y tests.

Comentarios
¿Quieres comentar?

Inicia sesión con Telegram para participar en la conversación


Comentarios (0)

Aún no hay comentarios. ¡Sé el primero en comentar!

Iniciar Sesión