Manejo de errores en Rust: desde unwrap hasta técnicas avanzadas
A diferencia de los lenguajes con excepciones (Java, Python, C++), Rust no utiliza el mecanismo try/catch. En su lugar, el lenguaje ofrece un sistema de tipos potente y seguro para el manejo de errores. Los desarrolladores principiantes suelen abusar de unwrap(), lo que provoca el pánico de la aplicación. En este artículo exploraremos técnicas avanzadas para trabajar con errores en Rust: desde la creación de tipos de error personalizados hasta el uso de las crates thiserror y anyhow, y analizaremos cómo emular el comportamiento de try/catch.
1. ¿Por qué Rust abandonó try/catch?
En los lenguajes con excepciones, el flujo de ejecución puede interrumpirse en cualquier momento. Esto dificulta el análisis del código y puede provocar fugas de recursos. Rust ofrece dos enfoques principales:
- Errores recuperables — el tipo
Result<T, E>(éxitoOk(T)o errorErr(E)). - Errores irrecuperables — la macro
panic!, que detiene el programa.
El operador ? es el equivalente de try en Rust. Extrae el valor de Ok o devuelve inmediatamente Err desde la función.
use std::fs::File;use std::io::Read;
fn read_username(path: &str) -> Result<String, std::io::Error> { let mut file = File::open(path)?; // si hay error, lo devolvemos let mut username = String::new(); file.read_to_string(&mut username)?; // nuevamente "try" Ok(username.trim().to_string())}2. Creación de tipos de error personalizados
Para proyectos complejos, un solo std::io::Error no es suficiente. La mejor práctica es definir tu propio tipo de error usando una enumeración (enum). Esto permite agrupar diferentes tipos de errores y agregar contexto.
use std::fmt;
#[derive(Debug)]enum MyAppError { IoError(std::io::Error), ParseError(String), NetworkError { url: String, status: u16 },}
impl fmt::Display for MyAppError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { MyAppError::IoError(e) => write!(f, "Error de E/S: {}", e), MyAppError::ParseError(s) => write!(f, "Error de análisis: {}", s), MyAppError::NetworkError { url, status } => { write!(f, "Error de red para {}: estado {}", url, status) } } }}
impl std::error::Error for MyAppError {} // implementamos el trait ErrorAhora podemos usar ? para la conversión automática, si implementamos el trait From:
impl From<std::io::Error> for MyAppError { fn from(err: std::io::Error) -> MyAppError { MyAppError::IoError(err) }}
fn process_file(path: &str) -> Result<(), MyAppError> { let content = std::fs::read_to_string(path)?; // se convierte automáticamente if content.is_empty() { return Err(MyAppError::ParseError("archivo vacío".into())); } Ok(())}3. Crates avanzadas: thiserror y anyhow
Escribir manualmente todas las implementaciones de Display y From es tedioso. Para esto existen dos crates populares:
thiserror — para bibliotecas
La crate thiserror genera la implementación de std::error::Error a través de una macro derive. Es ideal para bibliotecas donde se necesita una descripción precisa de los errores.
use thiserror::Error;
#[derive(Error, Debug)]pub enum DataError { #[error("Error de E/S: {0}")] Io(#[from] std::io::Error), #[error("UTF-8 inválido: {0}")] Utf8(#[from] std::str::Utf8Error), #[error("Fallo personalizado: {details}")] Custom { details: String },}
// Ahora podemos escribir:fn parse_data(bytes: &[u8]) -> Result<String, Da