Manejo de Errores

Enfoques Comunes para el Manejo de Errores en Aplicaciones Rust

El manejo de errores en Rust difiere de lenguajes como Java; carece de construcciones como try...catch. El enfoque típico es definir un tipo de error global a nivel de la aplicación:

use thiserror::Error;

#[derive(Error, Debug)]
pub enum AppError {
    #[error("io: `{0}`")]
    Io(#[from] io::Error),
    #[error("utf8: `{0}`")]
    FromUtf8(#[from] FromUtf8Error),
    #[error("diesel: `{0}`")]
    Diesel(#[from] diesel::result::Error),
    ...
}

pub type AppResult<T> = Result<T, AppError>;

Aquí se utiliza la biblioteca thiserror, que facilita la definición de tipos de error personalizados y simplifica el código. Por brevedad, también se define un alias de tipo AppResult.

thiserror vs. anyhow

En el ecosistema de manejo de errores de Rust, dos bibliotecas de uso común son thiserror y anyhow:

  • thiserror: Adecuada para desarrolladores de bibliotecas para definir tipos de error claros. Utiliza macros derive para ayudar a implementar el trait std::error::Error para tipos de error personalizados, permitiéndote definir representaciones de error. Al construir una biblioteca o proporcionar tipos de error claros a los usuarios, thiserror es la mejor opción.

  • anyhow: Orientada a desarrolladores de aplicaciones, proporciona un tipo de error genérico anyhow::Error que puede encapsular cualquier error que implemente el trait std::error::Error. Se centra más en la propagación de errores que en su definición, lo que la hace particularmente adecuada para código a nivel de aplicación. Puedes convertir rápidamente varios errores en anyhow::Error, reduciendo la necesidad de código repetitivo.

En algunos escenarios, podrías usar ambas bibliotecas: definir tipos de error con thiserror en bibliotecas y manejar y propagar estos errores con anyhow en aplicaciones.

Manejo de Errores en Handlers

En Salvo, los Handler a menudo encuentran varios errores, como errores de conexión a base de datos, errores de acceso a archivos, errores de conexión de red, etc. Para este tipo de errores, se puede aplicar el enfoque de manejo de errores mencionado anteriormente:

#[handler]
async fn home() -> AppResult<()> {

}

Aquí, home devuelve directamente un AppResult<()>. Pero, ¿cómo se debe mostrar este error? Necesitamos implementar el trait Writer para el tipo de error personalizado AppResult, donde podemos decidir cómo mostrar el error:

#[async_trait]
impl Writer for AppError {
    async fn write(mut self, _req: &mut Request, depot: &mut Depot, res: &mut Response) {
        res.render(Text::Plain("¡Soy un error, jajaja!"));
    }
}

En Salvo, un Handler puede devolver un Result, siempre que tanto el tipo Ok como el tipo Err en el Result implementen el trait Writer.

Manejo de Errores con anyhow

Dado el uso generalizado de anyhow, Salvo proporciona soporte integrado para anyhow::Error. Cuando la característica anyhow está habilitada, anyhow::Error implementa el trait Writer y se asigna a InternalServerError:

#[cfg(feature = "anyhow")]
#[async_trait]
impl Writer for ::anyhow::Error {
    async fn write(mut self, _req: &mut Request, _depot: &mut Depot, res: &mut Response) {
        res.render(StatusError::internal_server_error());
    }
}

Para usar la característica anyhow, habilita la característica anyhow de Salvo en Cargo.toml:

[dependencies]
salvo = { version = "*", features = ["anyhow"] }
anyhow = "1.0"

Esto permite que tus funciones handler devuelvan directamente anyhow::Result<T>:

#[handler]
async fn home() -> anyhow::Result<impl Writer> {
    let data = fetch_data().context("Error al obtener datos")?;
    Ok(Text::Plain(data))
}

Los errores a menudo contienen información sensible, que generalmente no debería ser visible para usuarios regulares por razones de seguridad y privacidad. Sin embargo, si eres un desarrollador o administrador del sitio, podrías preferir que los errores se expongan completamente, revelando la información de error más precisa.

Como se muestra, en el método write, podemos acceder a referencias de Request y Depot, lo que facilita la implementación del enfoque anterior:

#[async_trait]
impl Writer for AppError {
    async fn write(mut self, _req: &mut Request, depot: &mut Depot, res: &mut Response) {
        let user = depot.obtain::<User>();
        if user.is_admin {
            res.render(Text::Plain(e.to_string()));
        } else {
            res.render(Text::Plain("¡Soy un error, jajaja!"));
        }
    }
}

Mostrar Páginas de Error

Las páginas de error integradas de Salvo cumplen con los requisitos en la mayoría de los casos, mostrando páginas Html, Json o Xml según el tipo de datos de la solicitud. Sin embargo, hay situaciones en las que aún se desean pantallas de error personalizadas.

Esto se puede lograr implementando un Catcher personalizado. Para instrucciones detalladas, consulta la sección Catcher.