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::Displayystd::error::Errorpara 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.
