Manejo de errores en Rust en 2026: Result, Option, thiserror y anyhow
Guía completa sobre el manejo de errores en Rust con Result, Option, el operador ?, thiserror para bibliotecas y anyhow para aplicaciones.

El manejo de errores en Rust se centra en dos enumeraciones fundamentales: Result<T, E> y Option<T>. Estos tipos obligan al desarrollador a manejar explícitamente los casos de éxito, fallo y ausencia en tiempo de compilación. A diferencia de las excepciones que se propagan silenciosamente, el enfoque de Rust hace visibles las rutas de error en las firmas de funciones, eliminando categorías enteras de sorpresas en tiempo de ejecución. El operador ?, combinado con los crates thiserror y anyhow, simplifica este modelo explícito sin sacrificar la claridad.
Usar Option<T> para valores que pueden estar legítimamente ausentes (campos de configuración, resultados de búsqueda). Usar Result<T, E> cuando las operaciones pueden fallar con información de error significativa (E/S de archivos, solicitudes de red, parsing).
Comprender los fundamentos de Result y Option
Result<T, E> representa éxito (Ok(T)) o fallo (Err(E)). Option<T> representa un valor (Some(T)) o ausencia (None). Ambos son tipos suma — el compilador asegura que cada variante sea manejada.
// Demonstrates Result and Option basic patterns
fn find_user(id: u64) -> Option<String> {
// Returns None if user doesn't exist
if id == 0 {
None
} else {
Some(format!("User-{}", id))
}
}
fn parse_port(s: &str) -> Result<u16, std::num::ParseIntError> {
// Returns Err if parsing fails
s.parse::<u16>()
}
fn main() {
// Option handling - must address None case
match find_user(42) {
Some(name) => println!("Found: {}", name),
None => println!("User not found"),
}
// Result handling - must address Err case
match parse_port("8080") {
Ok(port) => println!("Port: {}", port),
Err(e) => println!("Invalid port: {}", e),
}
}El compilador rechaza código que ignora estos valores de retorno sin manejo explícito. Este diseño detecta bugs en tiempo de compilación que se manifestarían como excepciones de puntero nulo o errores no capturados en otros lenguajes.
El operador de interrogación para propagación concisa
El operador ? transforma cadenas verbosas de match en código lineal legible. Cuando se aplica a un Result, retorna anticipadamente con el error si está presente, o extrae el valor de éxito. Lo mismo aplica para Option.
// Using ? for clean error propagation
use std::fs::File;
use std::io::{self, BufRead, BufReader};
fn read_first_line(path: &str) -> Result<String, io::Error> {
let file = File::open(path)?; // Returns early if open fails
let mut reader = BufReader::new(file);
let mut line = String::new();
reader.read_line(&mut line)?; // Returns early if read fails
Ok(line.trim().to_string())
}
fn get_port_from_config(path: &str) -> Result<u16, Box<dyn std::error::Error>> {
let content = read_first_line(path)?;
let port = content.parse::<u16>()?; // ParseIntError converts via From
Ok(port)
}El operador ? requiere que el tipo de error sea convertible al tipo de error de retorno de la función a través del trait From. Usar Box<dyn std::error::Error> como se muestra arriba acepta cualquier tipo de error que implemente el trait Error estándar.
Crear errores personalizados con thiserror
El crate thiserror elimina el código repetitivo para tipos de error personalizados. Deriva las implementaciones de Error, Display y From a través de una macro procedural.
// Custom error types using thiserror 2.0
use thiserror::Error;
#[derive(Error, Debug)]
pub enum ConfigError {
#[error("configuration file not found at {path}")]
NotFound { path: String },
#[error("invalid port number: {0}")]
InvalidPort(#[from] std::num::ParseIntError),
#[error("IO error reading config")]
IoError(#[from] std::io::Error),
#[error("missing required field: {0}")]
MissingField(String),
}
fn load_config(path: &str) -> Result<Config, ConfigError> {
let content = std::fs::read_to_string(path)?; // IoError auto-converts
let port: u16 = content
.lines()
.find(|l| l.starts_with("port="))
.ok_or(ConfigError::MissingField("port".into()))?
.strip_prefix("port=")
.unwrap()
.parse()?; // ParseIntError auto-converts to InvalidPort
Ok(Config { port })
}
struct Config {
port: u16,
}El atributo #[from] genera conversiones automáticas, permitiendo el uso fluido de ? con diferentes tipos de error subyacentes. Los mensajes de error se vuelven autodocumentados a través de las cadenas de formato #[error(...)].
Errores a nivel de aplicación con anyhow
Mientras que thiserror es adecuado para código de biblioteca con tipos de error específicos, anyhow está orientado a aplicaciones donde el contexto del error importa más que la granularidad de tipos. Su trait Context agrega mensajes descriptivos a cualquier error.
// Application error handling with anyhow 1.0
use anyhow::{Context, Result, bail, ensure};
fn load_database_url() -> Result<String> {
std::env::var("DATABASE_URL")
.context("DATABASE_URL environment variable not set")
}
fn connect_to_database(url: &str) -> Result<DatabaseConnection> {
ensure!(!url.is_empty(), "database URL cannot be empty");
let conn = DatabaseConnection::new(url)
.context("failed to establish database connection")?;
if !conn.is_healthy() {
bail!("database connection unhealthy after establishment");
}
Ok(conn)
}
fn main() -> Result<()> {
let url = load_database_url()?;
let conn = connect_to_database(&url)
.context("application startup failed")?;
// Context chains create readable error traces:
// Error: application startup failed
// Caused by:
// 0: failed to establish database connection
// 1: connection refused
Ok(())
}
struct DatabaseConnection;
impl DatabaseConnection {
fn new(_url: &str) -> Result<Self> { Ok(Self) }
fn is_healthy(&self) -> bool { true }
}El método context() envuelve errores con información adicional, creando una cadena que facilita la depuración. La macro bail! proporciona una salida anticipada con un mensaje de error formateado, mientras que ensure! actúa como una aserción que retorna un error en lugar de entrar en pánico.
¿Listo para aprobar tus entrevistas de Rust?
Practica con nuestros simuladores interactivos, flashcards y tests técnicos.
Combinar thiserror y anyhow en proyectos reales
Las bibliotecas exponen errores estructurados a través de thiserror para el manejo programático por parte de los consumidores. Las aplicaciones envuelven esos errores con anyhow para una salida legible por humanos. Esta separación mantiene las APIs limpias mientras preserva la capacidad de depuración.
// Exposes typed errors for programmatic handling
use thiserror::Error;
#[derive(Error, Debug)]
pub enum PaymentError {
#[error("insufficient funds: required {required}, available {available}")]
InsufficientFunds { required: u64, available: u64 },
#[error("card declined: {reason}")]
CardDeclined { reason: String },
#[error("payment provider unavailable")]
ProviderUnavailable(#[source] reqwest::Error),
}
pub fn process_payment(amount: u64) -> Result<Receipt, PaymentError> {
// Library returns specific, matchable error types
Err(PaymentError::InsufficientFunds {
required: amount,
available: 50,
})
}
pub struct Receipt;// Wraps library errors with context
use anyhow::{Context, Result};
use my_payment_lib::{process_payment, PaymentError};
fn checkout(cart_total: u64) -> Result<()> {
match process_payment(cart_total) {
Ok(_receipt) => Ok(()),
Err(PaymentError::InsufficientFunds { required, available }) => {
// Handle specific case differently
println!("Add {} to your balance", required - available);
Ok(())
}
Err(e) => Err(e).context("checkout payment processing failed"),
}
}Este patrón permite a los llamadores hacer match en variantes específicas cuando la recuperación es posible, mientras se benefician del contexto de error enriquecido al propagar fallos hacia arriba. La comunidad de Rust ha estandarizado ampliamente este enfoque, como se discute en las directrices de API de Rust.
Patrones de manejo de errores para código asíncrono
Las funciones asíncronas retornan Result igual que las síncronas. El operador ? funciona de manera idéntica dentro de bloques async, y tanto thiserror como anyhow se integran sin modificación.
// Error handling in async Rust with Tokio
use anyhow::{Context, Result};
use std::time::Duration;
async fn fetch_user_data(user_id: u64) -> Result<UserData> {
let response = reqwest::get(format!("https://api.example.com/users/{}", user_id))
.await
.context("HTTP request to user API failed")?;
let status = response.status();
if !status.is_success() {
anyhow::bail!("user API returned status {}", status);
}
let data: UserData = response
.json()
.await
.context("failed to parse user data JSON")?;
Ok(data)
}
async fn fetch_with_retry(user_id: u64, attempts: u32) -> Result<UserData> {
let mut last_error = None;
for attempt in 1..=attempts {
match fetch_user_data(user_id).await {
Ok(data) => return Ok(data),
Err(e) => {
last_error = Some(e);
if attempt < attempts {
tokio::time::sleep(Duration::from_millis(100 * attempt as u64)).await;
}
}
}
}
Err(last_error.unwrap()).context(format!("failed after {} attempts", attempts))
}
#[derive(serde::Deserialize)]
struct UserData {
name: String,
}Al combinar múltiples operaciones asíncronas, usar try_join! de tokio o futures para ejecutarlas concurrentemente mientras se propaga el primer error.
Downcasting e inspección de errores
Tanto anyhow::Error como Box<dyn Error> soportan downcasting para recuperar el tipo de error original. Esto permite registrar detalles específicos mientras se propagan errores genéricos.
// Inspecting wrapped error types
use anyhow::{Context, Result};
use std::io;
fn log_and_propagate(result: Result<()>) -> Result<()> {
if let Err(ref e) = result {
// Check if the root cause is a specific type
if let Some(io_err) = e.downcast_ref::<io::Error>() {
match io_err.kind() {
io::ErrorKind::NotFound => {
tracing::warn!("file not found, using defaults");
}
io::ErrorKind::PermissionDenied => {
tracing::error!("permission denied - check file ownership");
}
_ => {
tracing::error!("IO error: {:?}", io_err);
}
}
}
}
result
}El downcasting cierra la brecha entre el manejo de errores genérico y la lógica de recuperación específica. Usarlo con moderación — si el downcasting ocurre frecuentemente, considerar si una enumeración de errores tipados serviría mejor.
Conversión entre Option y Result
La biblioteca estándar proporciona métodos para convertir entre Option y Result, permitiendo una composición fluida cuando diferentes APIs usan diferentes patrones.
// Option and Result interoperability
fn get_env_port() -> Option<u16> {
std::env::var("PORT")
.ok() // Result -> Option (discards error)
.and_then(|s| s.parse().ok())
}
fn get_env_port_with_error() -> Result<u16, String> {
std::env::var("PORT")
.map_err(|_| "PORT not set".to_string())?
.parse()
.map_err(|_| "PORT is not a valid number".to_string())
}
fn lookup_and_parse(map: &std::collections::HashMap<String, String>, key: &str) -> Result<u16, String> {
map.get(key)
.ok_or_else(|| format!("key '{}' not found", key))? // Option -> Result
.parse()
.map_err(|e| format!("parse error for '{}': {}", key, e))
}El método ok() descarta los detalles de error cuando solo importa la presencia. Los métodos ok_or() y ok_or_else() convierten None en un error personalizado, permitiendo la propagación con ? desde valores Option.
Consideraciones de rendimiento
El manejo de errores en Rust no tiene costo en tiempo de ejecución en el camino de éxito. Result y Option son enumeraciones asignadas en la pila con tamaño determinístico. El compilador optimiza las verificaciones cuando puede probar que una rama es inalcanzable.
| Enfoque | Costo camino éxito | Costo camino fallo | |---------|-------------------|--------------------| | Result/Option | Cero | Desenrollado de pila (barato) | | panic! | Cero | Desenrollado completo de pila + limpieza | | Excepciones C++ | Cero (generalmente) | Asignación heap costosa + RTTI |
Evitar unwrap() y expect() en código de biblioteca — reservarlos para casos donde el fallo genuinamente indica un bug. Para caminos críticos en rendimiento donde los errores son comunes, considerar usar enumeraciones con datos inline en lugar de tipos de error asignados en el heap.
Conclusión
Result<T, E>maneja fallos recuperables;Option<T>maneja ausencia — ambos imponen manejo en tiempo de compilación- El operador
?propaga errores de manera concisa, requiriendo implementaciones del traitFrompara conversión de tipos thiserrorgenera tipos de error estructurados para bibliotecas sin boilerplateanyhowproporciona cadenas de errores contextuales para aplicaciones, soportandocontext(),bail!yensure!- Combinar ambos: las bibliotecas exponen errores tipados vía
thiserror, las aplicaciones los envuelven conanyhow - El código asíncrono usa patrones idénticos —
?funciona en bloques async sin modificación - Usar
ok_or()para convertirOptionaResult; usarok()para lo inverso - Hacer downcast de errores envueltos solo cuando la lógica de recuperación específica requiere el tipo original
¡Empieza a practicar!
Pon a prueba tu conocimiento con nuestros simuladores de entrevista y tests técnicos.
Etiquetas
Compartir
Artículos relacionados

Smart pointers de Rust explicados: Box, Rc, Arc y RefCell en 2026
Los smart pointers de Rust Box, Rc, Arc y RefCell explicados con ejemplos compilables de 2026, una tabla de decisión y las preguntas de entrevista más comunes.

Rust 2026 Edition: Traits, Generics y Preguntas Avanzadas de Entrevista Tecnica
Domina traits, generics y las novedades de Rust 2024/2026 Edition con ejemplos de codigo y preguntas frecuentes en entrevistas tecnicas avanzadas de Rust.

Async/Await en Rust: Tokio, Futures y concurrencia asincrónica explicados a fondo
Análisis profundo de async/await en Rust: el runtime Tokio, el trait Future, el lanzamiento de tareas, la concurrencia estructurada y los patrones prácticos para construir aplicaciones asincrónicas de alto rendimiento.