En el desarrollo de aplicaciones profesionales en Rust, el uso de tipos de error genéricos como String o Box<dyn Error> es insuficiente para la lógica de negocio. Aunque Box<dyn Error> es útil para prototipado rápido o aplicaciones de línea de comandos simples donde el rendimiento y la inspección estructurada no son críticos, un software robusto requiere tipos de error propios.

Para lograrlo, se utiliza un enum donde cada variante representa una categoría de error específica de tu dominio. Esto permite que el llamador (caller) pueda realizar un pattern matching sobre el error para tomar decisiones programáticas (por ejemplo, reintentar si es un error de red, pero detenerse si es un error de sintaxis).

Para que un tipo sea considerado un error estándar en Rust, debe implementar dos traits obligatorios: std::fmt::Display (para mensajes legibles por humanos) y std::error::Error. Además, para permitir el encadenamiento de errores (error chaining), se debe implementar el método source() en el trait Error, el cual permite rastrear la causa raíz de un error. Finalmente, implementar el trait From<T> para los errores internos permite que el operador ? convierta automáticamente otros tipos de error (como io::Error) en tu tipo personalizado, manteniendo el código limpio y legible.

use std::fmt;
use std::error::Error;
use std::io;
use std::num::ParseIntError;

// 1. Definición del enum con las variantes de error
#[derive(Debug)]
enum MiError {
    Io(io::Error),
    Parseo(ParseIntError),
    Personalizado(String),
}

// 2. Implementación de Display para mensajes legibles
impl fmt::Display for MiError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            MiError::Io(e) => write!(f, "Error de entrada/salida: {}", e),
            MiError::Parseo(e) => write!(f, "Error al procesar número: {}", e),
            MiError::Personalizado(msg) => write!(f, "Error de aplicación: {}", msg),
        }
    }
}

// 3. Implementación de Error para integración con el ecosistema
impl Error for MiError {
    // Implementación de source para permitir el rastreo de la causa raíz
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            MiError::Io(e) => Some(e as &(dyn Error + 'static)),
            MiError::Parseo(e) => Some(e as &(dyn Error + 'static)),
            MiError::Personalizado(_) => None,
        }
    }
}

// 4. Implementación de From para permitir el uso del operador '?'
impl From<io::Error> for MiError {
    fn from(err: io::Error) -> Self {
        MiError::Io(err)
    }
}

impl From<ParseIntError> for MiError {
    fn from(err: ParseIntError) -> Self {
        MiError::Parseo(err)
    }
}

// Función que demuestra el uso de '?' con la conversión automática
fn procesar_configuracion(entrada: &str) -> Result<i32, MiError> {
    // El operador '?' usará impl From<io::Error> si read_to_string fallara
    // (Simulamos una operación de IO que podría fallar)
    let _dummy_io = std::fs::read_to_string("no_existe.txt")?; 

    // El operador '?' usará impl From<ParseIntError> si el parse falla
    let numero: i32 = entrada.trim().parse()?;

    if numero < 0 {
        return Err(MiError::Personalizado("El número no puede ser negativo".to_string()));
    }

    Ok(numero)
}

fn main() {
    let dato = " -5 ";
    
    match procesar_configuracion(dato) {
        Ok(n) => println!("Valor procesado: {}", n),
        Err(e) => {
            eprintln!("Error detectado: {}", e);
            if let Some(causa) = e.source() {
                eprintln!("Causa raíz: {}", causa);
            }
        }
    }
}

Explicación del Código

  • enum MiError: Define nuestra jerarquía de errores. Contiene variantes que encapsulan otros tipos de error (io::Error, ParseIntError) o un mensaje genérico (String).
  • impl fmt::Display for MiError: Define cómo se verá el error al imprimirse con {}. Usamos un match self para formatear cada variante de forma distinta.
  • impl Error for MiError: Proporciona la interfaz estándar de error. El método source() es crucial: mediante un casting a &(dyn Error + 'static), permite que el programador acceda a la causa original del error (por ejemplo, el error de I/O real que provocó nuestro MiError::Io).
  • impl From<io::Error> for MiError y impl From<ParseIntError> for MiError: Estas implementaciones habilitan la “magia” del operador ?. Cuando una función devuelve Result<T, io::Error> y se llama dentro de una función que devuelve Result<T, MiError>, el compilador invoca automáticamente MiError::from(err).
  • procesar_configuracion: Esta función demuestra la potencia del diseño. Gracias a las implementaciones de From, podemos usar ? de manera fluida, delegando la conversión de errores complejos a nuestra lógica personalizada.
  • e.source() en main: Permite inspeccionar la cadena de errores. Si el error fuera un io::Error, source() nos devolvería la causa técnica original.

61