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
| Aspecto | Excepciones (Java/Python/JS) | Result (Rust) |
|---|---|---|
| Coste en el camino feliz | Cero (salvo setup) | Cero (enum en el stack) |
| Coste al lanzar excepción | Alto (stack unwinding, objeto excepción) | Cero (es un valor) |
| Visibilidad en firma | Solo con checked exceptions (Java) | Siempre |
| Olvidar manejar el error | Posible (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;
Resultes explícito. - El operador
?propaga el error con la misma ergonomía que try/catch. Resulten el tipo de retorno documenta que la función puede fallar.- El rendimiento de
Resultes igual o mejor que las excepciones. panic!para bugs del programador;Resultpara 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.
