rust-result-ok-err-manejo-fallos

Result en Rust: gestionar errores con Ok, Err y map

  • 5 min

Un Result es un tipo para representar éxito o fallo en una operación.

En el artículo sobre panic! introdujimos Result, y en el anterior vimos Option para valores ausentes. Ahora vamos a profundizar en Result para diseñar nuestras propias funciones que pueden fallar, usar alias de tipos y manipular errores con estilo funcional.

Ya sabemos que en Rust los errores son valores. Específicamente, son variantes del enum Result<T, E>:

enum Result<T, E> {
    Ok(T),  // Todo salió bien, aquí tienes el dato.
    Err(E), // Algo falló, aquí tienes la razón.
}
Copied!

Hasta ahora hemos sido consumidores de Result (usando funciones de la librería estándar como File::open). Hoy vamos a crear nuestros propios resultados y a transformarlos sin perder la información del error.

Escribiendo funciones que fallan

Imagina que queremos hacer una función para dividir. Matemáticamente, dividir por cero no está definido. En C++ o Java, lanzarías una excepción. En C, devolverías un código de error raro o NaN.

En Rust, devolvemos un Result.

fn dividir(numerador: f64, denominador: f64) -> Result<f64, String> {
    if denominador == 0.0 {
        // Construimos el error envolviéndolo en Err
        return Err(String::from("No se puede dividir por cero"));
    }

    // Devolvemos el éxito envuelto en Ok
    Ok(numerador / denominador)
}

fn main() {
    let resultado = dividir(10.0, 0.0);

    match resultado {
        Ok(valor) => println!("Resultado: {}", valor),
        Err(mensaje) => println!("Error: {}", mensaje),
    }
}
Copied!

Fíjate en la firma: Result<f64, String>.

  • T (Éxito) es f64.
  • E (Error) es String (un mensaje de texto simple).

Alias de tipos

Si escribes mucho código que usa la librería estándar std::io (entrada/salida), verás que muchas funciones devuelven io::Result<T>. ¿Dónde está la E?

Rust permite crear Alias de Tipos para no repetir código. La librería estándar hace esto para ahorrarte escribir el tipo de error una y otra vez. En el módulo io, el error siempre es std::io::Error.

Tú puedes hacer lo mismo en tus módulos:

// Definimos un alias específico para nuestra lógica matemática
type MathResult<T> = Result<T, String>;

// Ahora la firma es mucho más limpia
fn dividir(a: f64, b: f64) -> MathResult<f64> {
    if b == 0.0 {
        return Err(String::from("División por cero"));
    }
    Ok(a / b)
}
Copied!

Transformar éxitos y errores

El match es muy expresivo, pero a veces verboso. Rust nos ofrece una serie de combinadores (métodos funcionales) inspirados en Option, pero adaptados para gestionar el éxito o el error sin desempaquetar la caja.

map: transformar el éxito

Imagina que llamas a una función que devuelve Result<i32, String>. Si funciona, quieres multiplicar el número por 10. Si falla, quieres dejar el error tal cual.

let resultado: Result<i32, String> = Ok(5);

// Si es Ok(5) -> se convierte en Ok("50")
// Si fuera Err("mal") -> se quedaría en Err("mal")
let texto = resultado.map(|n| (n * 10).to_string());
Copied!

map_err: transformar el error

Este es muy útil cuando trabajamos con distintas librerías. A veces recibimos un error de “Base de Datos” pero nuestra función promete devolver un error de “API”. Necesitamos transformar la E.

fn procesar_dato() -> Result<i32, String> {
    let valor = funcion_que_falla().map_err(|e| {
        format!("Error interno de librería: {}", e)
    })?; // El operador ? ahora propaga nuestro nuevo error String

    Ok(valor)
}
Copied!

unwrap_or y unwrap_or_else

Igual que con Option, podemos proveer valores de respaldo (fallback) en caso de error.

let config = leer_configuracion().unwrap_or_else(|_| Configuracion::por_defecto());
Copied!

Esto es muy útil para tolerar fallos no críticos.

De Option a Result y viceversa

A veces tienes un Option (algo que puede no estar) pero tu función necesita devolver un Result (un error explicativo).

El método .ok_or(error) transforma un Option en un Result.

fn buscar_usuario(id: i32) -> Option<String> {
    if id == 1 { Some("Luis".to_string()) } else { None }
}

fn loguear_usuario(id: i32) -> Result<(), String> {
    // Transformamos el None en un Err("Usuario no encontrado")
    let usuario = buscar_usuario(id).ok_or("Usuario no encontrado")?;

    println!("Usuario logueado: {}", usuario);
    Ok(())
}
Copied!

También existe el inverso: .ok() transforma un Result en un Option, descartando la información del error (convirtiendo Err en None).

let archivo = File::open("no_existe.txt").ok(); // Option<File> es None
Copied!

Buenas prácticas: No uses String para errores

En los ejemplos de arriba he usado String como tipo de error (Result<T, String>) por simplicidad.

Sin embargo, en código real (especialmente librerías), esto es una mala práctica.

  • Un String suele requerir una asignación de memoria.
  • Los Strings no son fáciles de manejar por código (no puedes hacer match error fácilmente para distinguir un error de “Permiso denegado” de uno de “Disco lleno”).

La forma correcta es usar un Enum para tus errores, o usar tipos de error estándar.

#[derive(Debug)]
enum ErrorMatematico {
    DivisionPorCero,
    RaizNegativa,
    Desbordamiento,
}

fn dividir(a: f64, b: f64) -> Result<f64, ErrorMatematico> {
    if b == 0.0 {
        return Err(ErrorMatematico::DivisionPorCero);
    }
    Ok(a / b)
}
Copied!

Esto permite a quien use tu función reaccionar específicamente:

match dividir(10.0, 0.0) {
    Ok(val) => println!("{}", val),
    Err(ErrorMatematico::DivisionPorCero) => println!("¡Cuidado con el cero!"),
    Err(_) => println!("Otro error matemático"),
}
Copied!