Manejo de errores en Rust: renunciar a try/catch en favor de la seguridad
A diferencia de Python o Java, en Rust no existe la construcción clásica try/catch. Los desarrolladores del lenguaje tomaron otro camino, haciendo del manejo de errores parte del sistema de tipos. Esto no es un capricho, sino una decisión fundamental que permite garantizar la seguridad de la memoria y la previsibilidad del programa en tiempo de compilación. En este artículo, analizaremos todas las formas de manejar errores en Rust: desde los básicos Result y Option hasta métodos avanzados con el operador ? y tipos de error personalizados.
1. Dos tipos principales: Result y Option
En Rust, todos los errores se dividen en dos categorías: recuperables (se manejan mediante Result) y no recuperables (provocan panic!). También existe el tipo Option para casos donde un valor puede estar ausente.
Tipo Result
Result<T, E> es una enumeración (enum) que puede ser Ok(T) (éxito con datos) o Err(E) (error con descripción). Es la herramienta principal para manejar errores esperados: errores de entrada/salida, análisis sintáctico, solicitudes de red.
use std::fs::File;use std::io::Read;
fn read_file_content(path: &str) -> Result<String, std::io::Error> { let mut file = File::open(path)?; let mut content = String::new(); file.read_to_string(&mut content)?; Ok(content)}Tipo Option
Option<T> es otro enum que toma los valores Some(T) (el valor existe) o None (el valor no existe). Se utiliza cuando la ausencia de datos es una situación normal, no un error.
fn find_user(id: u32) -> Option<String> { let users = vec!["Alice".to_string(), "Bob".to_string()]; users.get(id as usize).cloned()}
fn main() { match find_user(0) { Some(name) => println!("Usuario encontrado: {}", name), None => println!("Usuario no encontrado"), }}2. Operador ? — elegante reemplazo de try/catch
Si Rust tuviera try/catch, sería engorroso. En su lugar, el lenguaje ofrece el operador ?. Funciona así: si el resultado es Ok, el valor se extrae y se pasa adelante; si es Err, el error se devuelve inmediatamente desde la función.
use std::num::ParseIntError;
fn parse_and_double(input: &str) -> Result<i32, ParseIntError> { let value: i32 = input.parse()?; // si hay error — return Err inmediato Ok(value * 2)}
fn main() { match parse_and_double("42") { Ok(result) => println!("Resultado: {}", result), Err(e) => println!("Error de análisis: {}", e), }}El operador ? solo se puede usar en funciones que devuelven Result o Option. Esto obliga al desarrollador a indicar explícitamente que la función puede fallar — a diferencia de las excepciones en Python, que pueden "saltar" desde cualquier lugar.
3. Cuándo usar panic! y unwrap
No todos los errores necesitan ser manejados. panic! es una macro para errores no recuperables. Detiene el programa y desenrolla la pila (stack unwind). Úsalo solo en situaciones excepcionales:
- Estados críticos: violación de invariantes que no se pueden corregir.
- Ejemplos y prototipos: en código educativo o scripts de un solo uso.
- Pruebas: para verificar que el código falla en los lugares esperados.
fn divide(a: i32, b: i32) -> i32 { if b == 0 { panic!("¡División por cero! a={}, b={}", a, b); } a / b}unwrap() y expect()