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/eyrepara ergonomía. - No uses
unwrap()niexpect()en producción salvo que sea verdaderamente inmutable. - Añade contexto a los errores (mensajes útiles, campo
sourcecuando 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::Contextomap_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
Stringcomo 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.
¿Quieres comentar?
Inicia sesión con Telegram para participar en la conversación