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.

Manejo de errores en Rust con Result, Option, thiserror y anyhow

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.

Cuándo usar cuál

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.

error_examples.rsrust
// 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.

file_reader.rsrust
// 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.

errors.rsrust
// 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.

main.rsrust
// 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.

lib.rs - Library code with thiserrorrust
// 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;
main.rs - Application code with anyhowrust
// 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.

async_errors.rsrust
// 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.

downcasting.rsrust
// 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.

conversions.rsrust
// 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 trait From para conversión de tipos
  • thiserror genera tipos de error estructurados para bibliotecas sin boilerplate
  • anyhow proporciona cadenas de errores contextuales para aplicaciones, soportando context(), bail! y ensure!
  • Combinar ambos: las bibliotecas exponen errores tipados vía thiserror, las aplicaciones los envuelven con anyhow
  • El código asíncrono usa patrones idénticos — ? funciona en bloques async sin modificación
  • Usar ok_or() para convertir Option a Result; usar ok() 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

#rust
#manejo-errores
#result
#option
#thiserror
#anyhow

Compartir

Artículos relacionados