En esta página

Manejo de errores: Result, Option y el operador ?

14 min lectura TextoCap. 4 — Abstracciones

Manejo de errores en Rust

Rust no tiene excepciones. En cambio, usa tipos del sistema de tipos para representar operaciones que pueden fallar: Result<T, E> y Option<T>. Este enfoque obliga al programador a considerar los casos de error explícitamente, eliminando toda una clase de bugs.

Recordatorio: Result y Option

// En la stdlib:
// enum Result<T, E> { Ok(T), Err(E) }
// enum Option<T> { Some(T), None }

fn main() {
    // Result para operaciones que pueden fallar con un error específico
    let resultado: Result<i32, &str> = Ok(42);
    let error: Result<i32, &str> = Err("algo salió mal");
    
    // Option para valores que pueden no existir
    let presente: Option<&str> = Some("hola");
    let ausente: Option<&str> = None;
    
    // Ambos son enums normales — se manejan con match
    match resultado {
        Ok(n)  => println!("Éxito: {n}"),
        Err(e) => println!("Error: {e}"),
    }
    
    match presente {
        Some(s) => println!("Valor: {s}"),
        None    => println!("Sin valor"),
    }
}

El operador `?`: propagación elegante de errores

El operador ? es la pieza más importante del manejo de errores en Rust. Hace dos cosas:

  1. Si el valor es Ok(T), extrae T y continúa
  2. Si el valor es Err(E), convierte el error (via From) y retorna inmediatamente
use std::fs;
use std::io;

// Sin ? — verboso y ruidoso
fn leer_archivo_verboso(ruta: &str) -> Result<String, io::Error> {
    let contenido = match fs::read_to_string(ruta) {
        Ok(s)  => s,
        Err(e) => return Err(e),
    };
    Ok(contenido.to_uppercase())
}

// Con ? — limpio y directo
fn leer_archivo(ruta: &str) -> Result<String, io::Error> {
    let contenido = fs::read_to_string(ruta)?;
    Ok(contenido.to_uppercase())
}

// Encadenando múltiples operaciones que pueden fallar
fn procesar_numero(s: &str) -> Result<f64, Box<dyn std::error::Error>> {
    let n: i32 = s.trim().parse()?;  // ParseIntError → Box<dyn Error>
    let raiz = if n >= 0 {
        (n as f64).sqrt()
    } else {
        return Err("No se puede calcular raíz de negativo".into());
    };
    Ok(raiz)
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    match procesar_numero("  16  ") {
        Ok(r) => println!("Raíz: {r}"),
        Err(e) => println!("Error: {e}"),
    }
    
    match procesar_numero("abc") {
        Ok(r) => println!("Raíz: {r}"),
        Err(e) => println!("Error: {e}"),
    }
    
    Ok(())
}

Tipos de error personalizados

Para proyectos reales, querrás crear tipos de error descriptivos:

use std::fmt;
use std::num::ParseIntError;

#[derive(Debug)]
enum AppError {
    EntradaInvalida { campo: String, mensaje: String },
    BaseDeDatos(String),
    Red { codigo: u16, url: String },
    Parse(ParseIntError),
}

impl fmt::Display for AppError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            AppError::EntradaInvalida { campo, mensaje } => {
                write!(f, "Entrada inválida en '{campo}': {mensaje}")
            }
            AppError::BaseDeDatos(msg) => write!(f, "Error de base de datos: {msg}"),
            AppError::Red { codigo, url } => {
                write!(f, "Error de red {codigo} al conectar con {url}")
            }
            AppError::Parse(e) => write!(f, "Error de parseo: {e}"),
        }
    }
}

// Implementar std::error::Error para AppError
impl std::error::Error for AppError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            AppError::Parse(e) => Some(e),
            _ => None,
        }
    }
}

// Conversión automática desde ParseIntError
impl From<ParseIntError> for AppError {
    fn from(e: ParseIntError) -> Self {
        AppError::Parse(e)
    }
}

fn validar_edad(s: &str) -> Result<u8, AppError> {
    let edad: i32 = s.parse()?; // ParseIntError se convierte via From
    
    if edad < 0 || edad > 150 {
        return Err(AppError::EntradaInvalida {
            campo: String::from("edad"),
            mensaje: format!("{edad} no es una edad válida (0-150)"),
        });
    }
    
    Ok(edad as u8)
}

fn main() {
    for entrada in ["25", "abc", "-5", "200"] {
        match validar_edad(entrada) {
            Ok(edad) => println!("Edad válida: {edad}"),
            Err(e)   => println!("Error: {e}"),
        }
    }
}

Métodos útiles de Result y Option

fn main() {
    let ok: Result<i32, &str> = Ok(42);
    let err: Result<i32, &str> = Err("fallo");
    
    // --- Métodos de Result ---
    
    // unwrap: extrae el valor o entra en pánico
    println!("{}", ok.unwrap());
    
    // expect: como unwrap pero con mensaje personalizado
    println!("{}", ok.expect("Debería ser Ok"));
    
    // unwrap_or: valor por defecto si es Err
    println!("{}", err.unwrap_or(0));
    
    // unwrap_or_else: calcular valor por defecto
    println!("{}", err.unwrap_or_else(|e| { println!("Error: {e}"); -1 }));
    
    // map: transformar el valor Ok
    let doble = ok.map(|n| n * 2);
    println!("{:?}", doble);
    
    // map_err: transformar el error
    let transformado = err.map_err(|e| format!("ERROR: {e}"));
    println!("{:?}", transformado);
    
    // and_then: encadenar Results
    let resultado = ok
        .and_then(|n| if n > 0 { Ok(n * 10) } else { Err("negativo") })
        .and_then(|n| Ok(format!("Resultado: {n}")));
    println!("{:?}", resultado);
    
    // is_ok / is_err
    println!("¿ok es Ok? {}", ok.is_ok());
    println!("¿err es Err? {}", err.is_err());
    
    // --- Métodos de Option ---
    let some: Option<i32> = Some(10);
    let none: Option<i32> = None;
    
    println!("{}", some.unwrap_or(0));
    println!("{}", none.unwrap_or_default()); // i32::default() = 0
    
    // ok_or: convertir Option a Result
    let r: Result<i32, &str> = none.ok_or("no hay valor");
    println!("{:?}", r);
    
    // filter: conservar Some solo si cumple condición
    let par = some.filter(|n| n % 2 == 0);
    println!("{:?}", par); // None (10 no es par... espera, sí lo es)
    
    // zip: combinar dos Options
    let a = Some(1);
    let b = Some("hola");
    println!("{:?}", a.zip(b)); // Some((1, "hola"))
}

Panic: cuándo y cuándo no usarlo

fn main() {
    // panic! para situaciones que "no deberían ocurrir"
    // (errores de programación, no errores de usuario)
    let v = vec![1, 2, 3];
    // v[10]; // panic: index out of bounds
    
    // assert! para precondiciones
    fn calcular_raiz(n: f64) -> f64 {
        assert!(n >= 0.0, "No se puede calcular raíz de {n}");
        n.sqrt()
    }
    
    // todo! para código no implementado aún
    fn funcion_pendiente() -> i32 {
        todo!("Implementar lógica de negocio")
    }
    
    // unreachable! para ramas que no deberían alcanzarse
    let estado = 1_u8;
    let mensaje = match estado {
        0 => "inactivo",
        1 => "activo",
        2 => "suspendido",
        _ => unreachable!("Estado inválido: {estado}"),
    };
    
    println!("Estado: {mensaje}");
    println!("{:.4}", calcular_raiz(16.0));
}

La cadena completa: manejo de errores idiomático

use std::collections::HashMap;

#[derive(Debug)]
enum Error {
    ClaveFaltante(String),
    ValorInvalido { clave: String, valor: String },
}

impl std::fmt::Display for Error {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Error::ClaveFaltante(k) => write!(f, "Clave faltante: '{k}'"),
            Error::ValorInvalido { clave, valor } => {
                write!(f, "Valor inválido para '{clave}': '{valor}'")
            }
        }
    }
}

fn obtener_numero(config: &HashMap<&str, &str>, clave: &str) -> Result<i64, Error> {
    let valor = config.get(clave)
        .ok_or_else(|| Error::ClaveFaltante(clave.to_string()))?;
    
    valor.parse::<i64>()
        .map_err(|_| Error::ValorInvalido {
            clave: clave.to_string(),
            valor: valor.to_string(),
        })
}

fn main() {
    let config: HashMap<&str, &str> = [
        ("puerto", "8080"),
        ("timeout", "abc"),
    ].into_iter().collect();
    
    for clave in ["puerto", "timeout", "host"] {
        match obtener_numero(&config, clave) {
            Ok(n)  => println!("{clave} = {n}"),
            Err(e) => println!("Error: {e}"),
        }
    }
}

Con un manejo de errores robusto en tu arsenal, la próxima lección explora módulos y crates — cómo Rust organiza el código en unidades reutilizables.

El operador ? solo funciona en funciones que retornan Result u Option
Si usas ? en main(), debes cambiar la firma a fn main() -> Result<(), Box<dyn std::error::Error>>. Esto permite propagar errores directamente desde main y los imprime automáticamente si ocurren.
Evita unwrap() y expect() en código de producción
unwrap() y expect() entran en pánico si el valor es Err o None. Son útiles en prototipos, tests y cuando tienes certeza absoluta de que el valor existe. En código de producción, propaga el error con ? o manéjalo con match/if let.
rust
use std::num::ParseIntError;
use std::fmt;

// Error personalizado
#[derive(Debug)]
enum ErrorCalculo {
    DivisionPorCero,
    NumeroInvalido(ParseIntError),
    NegativoNoPermitido(i64),
}

impl fmt::Display for ErrorCalculo {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::DivisionPorCero => write!(f, "División por cero"),
            Self::NumeroInvalido(e) => write!(f, "Número inválido: {e}"),
            Self::NegativoNoPermitido(n) => {
                write!(f, "Negativo no permitido: {n}")
            }
        }
    }
}

impl From<ParseIntError> for ErrorCalculo {
    fn from(e: ParseIntError) -> Self {
        ErrorCalculo::NumeroInvalido(e)
    }
}

fn dividir(a: i64, b: i64) -> Result<i64, ErrorCalculo> {
    if b == 0 { return Err(ErrorCalculo::DivisionPorCero); }
    if a < 0  { return Err(ErrorCalculo::NegativoNoPermitido(a)); }
    Ok(a / b)
}

fn parsear_y_dividir(a: &str, b: &str) -> Result<i64, ErrorCalculo> {
    let a: i64 = a.trim().parse()?; // ? convierte via From
    let b: i64 = b.trim().parse()?;
    dividir(a, b)
}

fn main() {
    let casos = [("100", "4"), ("abc", "2"), ("50", "0"), ("-10", "2")];
    for (a, b) in &casos {
        match parsear_y_dividir(a, b) {
            Ok(r)  => println!("{a}/{b} = {r}"),
            Err(e) => println!("Error: {e}"),
        }
    }
}