Errores personalizados en Rust: implementar Display y Error para tus tipos

Cuando escribes una librería en Rust, Box<dyn Error> o String como tipo de error no son suficientes: pierdes información sobre el tipo exacto del error y el llamador no puede distinguir los casos. La solución es definir tus propios tipos de error implementando los traits Display y std::error::Error.

El mínimo: un struct con Display y Error

use std::fmt;

#[derive(Debug)]
struct MiError {
    mensaje: String,
}

impl fmt::Display for MiError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{}", self.mensaje)
    }
}

impl std::error::Error for MiError {}
// La implementación vacía está bien: Error solo requiere Debug + Display

fn puede_fallar(ok: bool) -> Result<(), MiError> {
    if ok {
        Ok(())
    } else {
        Err(MiError { mensaje: String::from("algo falló") })
    }
}

Enum de errores para múltiples causas

Si tu librería puede fallar de varias formas distintas, un enum es la solución idiomática:

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

#[derive(Debug)]
enum ErrorApp {
    Io(io::Error),
    ParseNumero(ParseIntError),
    ValorInvalido(String),
}

impl fmt::Display for ErrorApp {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            ErrorApp::Io(e)            => write!(f, "Error de I/O: {}", e),
            ErrorApp::ParseNumero(e)   => write!(f, "Error al parsear número: {}", e),
            ErrorApp::ValorInvalido(s) => write!(f, "Valor inválido: {}", s),
        }
    }
}

impl std::error::Error for ErrorApp {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            ErrorApp::Io(e)          => Some(e),
            ErrorApp::ParseNumero(e) => Some(e),
            ErrorApp::ValorInvalido(_) => None,
        }
    }
}

El método source()

source() devuelve la causa raíz del error (el error que provocó éste). Permite recorrer la cadena de errores para diagnóstico:

fn imprimir_cadena_error(e: &dyn std::error::Error) {
    println!("Error: {}", e);
    let mut causa = e.source();
    while let Some(c) = causa {
        println!("  Causado por: {}", c);
        causa = c.source();
    }
}

Conversión automática con From

impl From<io::Error> for ErrorApp {
    fn from(e: io::Error) -> Self {
        ErrorApp::Io(e)
    }
}

impl From<ParseIntError> for ErrorApp {
    fn from(e: ParseIntError) -> Self {
        ErrorApp::ParseNumero(e)
    }
}

// Ahora ? convierte automáticamente
fn procesar(ruta: &str) -> Result<i32, ErrorApp> {
    let contenido = std::fs::read_to_string(ruta)?; // io::Error ? ErrorApp::Io
    let numero: i32 = contenido.trim().parse()?;    // ParseIntError ? ErrorApp::ParseNumero
    if numero < 0 {
        return Err(ErrorApp::ValorInvalido(format!("Negativo: {}", numero)));
    }
    Ok(numero)
}

Constructores de conveniencia

impl ErrorApp {
    fn invalido(msg: impl Into<String>) -> Self {
        ErrorApp::ValorInvalido(msg.into())
    }
}

// En uso:
return Err(ErrorApp::invalido("El número debe ser positivo"));

Resumen

  • Implementa fmt::Display y std::error::Error para crear tipos de error propios.
  • Usa un struct para un único tipo de error, un enum para múltiples causas.
  • source() proporciona la cadena de causas para el diagnóstico.
  • Implementa From<E> para cada error subyacente para que ? los convierta automáticamente.
  • Añade constructores de conveniencia para simplificar la creación de errores.

El siguiente artículo presenta thiserror y anyhow: las dos librerías estándar de facto que eliminan el boilerplate de crear tipos de error propios y propagar errores en aplicaciones Rust.

COMPARTE ESTE ARTÍCULO

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