Result vs excepciones: cómo Rust cambia la forma de pensar los errores

Las excepciones son una forma de control de flujo implícito: cualquier función puede lanzar una excepción en cualquier punto, y el compilador de Java, Python o JavaScript no te obliga a saber cuáles pueden fallar. Rust toma una decisión diferente: los errores son valores. Result<T, E> hace que los errores sean visibles en la firma de cada función y el compilador te obliga a manejarlos.

El problema de las excepciones

// Java: no sabes qué puede lanzar sin leer la implementación
String contenido = leerFichero("datos.txt");   // puede lanzar IOException
int numero = Integer.parseInt(contenido.trim()); // puede lanzar NumberFormatException
procesar(numero);                                // quizás lanza RuntimeException

Las excepciones son control de flujo invisible. El código se ve secuencial pero cualquier línea puede saltar a un catch lejano. Esto hace que razonar sobre el flujo del programa sea más difícil.

Rust: errores explícitos en la firma

fn procesar_fichero(ruta: &str) -> Result<i32, Box<dyn std::error::Error>> {
    let contenido = std::fs::read_to_string(ruta)?; // puede fallar: Result
    let numero: i32 = contenido.trim().parse()?;    // puede fallar: Result
    Ok(numero * 2)
}

La firma dice exactamente qué puede fallar. El llamador sabe sin leer la implementación que esta función puede retornar un error. El compilador te obliga a manejarlo.

El operador ? equivale a try/catch localizado

// Esto:
let contenido = std::fs::read_to_string(ruta)?;

// Es equivalente a esto (en Java):
String contenido;
try {
    contenido = leerFichero(ruta);
} catch (IOException e) {
    return Err(e.into()); // propaga el error
}

La diferencia clave: en Rust sabes exactamente dónde puede fallar. En Java, el error puede venir de cualquier profundidad del stack.

Cuándo usar panic! en lugar de Result

// panic!: para invariantes del programador
fn dividir(a: f64, b: f64) -> f64 {
    assert!(b != 0.0, "El divisor no puede ser cero (bug del programador)");
    a / b
}

// Result: para errores esperados
fn parsear_entrada(s: &str) -> Result<f64, std::num::ParseFloatError> {
    s.trim().parse()
}

panic! es para situaciones que indican un bug en el código (invariante rota). Result es para errores que pueden ocurrir en uso normal y que el llamador debe poder manejar.

Rendimiento: Result vs excepciones

AspectoExcepciones (Java/Python/JS)Result (Rust)
Coste en el camino felizCero (salvo setup)Cero (enum en el stack)
Coste al lanzar excepciónAlto (stack unwinding, objeto excepción)Cero (es un valor)
Visibilidad en firmaSolo con checked exceptions (Java)Siempre
Olvidar manejar el errorPosible (unchecked exceptions)Warning del compilador

En el camino feliz (sin errores), ambos son igualmente eficientes. Cuando hay errores, Result es más rápido porque no construye un objeto excepción ni hace stack unwinding.

Composición de errores

use thiserror::Error;

#[derive(Error, Debug)]
enum ErrorApp {
    #[error("Error de I/O: {0}")]
    Io(#[from] std::io::Error),
    #[error("Número inválido: {0}")]
    Parse(#[from] std::num::ParseIntError),
}

fn leer_numero(ruta: &str) -> Result<i32, ErrorApp> {
    let s = std::fs::read_to_string(ruta)?; // io::Error ? ErrorApp::Io
    Ok(s.trim().parse()?)                   // ParseIntError ? ErrorApp::Parse
}

Cuándo Result cambia cómo piensas

La diferencia más profunda no es técnica sino cognitiva. Con excepciones, el camino feliz y el camino de error son conceptualmente separados. Con Result, son la misma cosa: una función retorna un valor, y ese valor puede ser el resultado esperado o un error. No hay control de flujo oculto.

Eso te obliga a pensar en los casos de error cuando diseñas la interfaz de una función, no después. Con el tiempo, este hábito produce APIs más robustas y código más predecible.

Resumen

  • Las excepciones son control de flujo implícito; Result es explícito.
  • El operador ? propaga el error con la misma ergonomía que try/catch.
  • Result en el tipo de retorno documenta que la función puede fallar.
  • El rendimiento de Result es igual o mejor que las excepciones.
  • panic! para bugs del programador; Result para errores esperados en runtime.

Con este artículo se cierra la serie "Rust Book" de programacion.net. Desde el ownership hasta los traits de concurrencia, todos los conceptos se apoyan en las mismas ideas fundamentales: la memoria es segura por diseño, los errores son valores y el compilador es tu aliado más exigente.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP