Tratamento de Erros

Abordagens Comuns de Tratamento de Erros em Aplicações Rust

O tratamento de erros em Rust difere de linguagens como Java; ele não possui construções como try...catch. A abordagem típica é definir um tipo de erro global no nível da aplicação:

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>;

Aqui, a biblioteca thiserror é utilizada, facilitando a definição de tipos de erro personalizados e simplificando o código. Para concisão, também é definido um alias de tipo AppResult.

thiserror vs. anyhow

No ecossistema de tratamento de erros do Rust, duas bibliotecas comumente usadas são thiserror e anyhow:

  • thiserror: Adequada para desenvolvedores de bibliotecas definirem tipos de erro claros. Ela usa macros derive para ajudar a implementar a trait std::error::Error para tipos de erro personalizados, permitindo que você defina representações de erro. Ao construir uma biblioteca ou fornecer tipos de erro claros aos usuários, thiserror é a melhor escolha.

  • anyhow: Voltada para desenvolvedores de aplicações, fornece um tipo de erro genérico anyhow::Error que pode encapsular qualquer erro que implemente a trait std::error::Error. Foca mais na propagação do erro do que na definição, tornando-a particularmente adequada para código de camada de aplicação. Você pode converter rapidamente vários erros em anyhow::Error, reduzindo a necessidade de código repetitivo.

Em alguns cenários, você pode usar ambas as bibliotecas: definir tipos de erro com thiserror em bibliotecas e lidar e propagar esses erros com anyhow em aplicações.

Tratamento de Erros em Handlers

No Salvo, Handlers frequentemente encontram vários erros, como erros de conexão com banco de dados, erros de acesso a arquivos, erros de conexão de rede, etc. Para esses tipos de erros, a abordagem de tratamento de erros mencionada anteriormente pode ser aplicada:

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

}

Aqui, home retorna diretamente um AppResult<()>. Mas como esse erro deve ser exibido? Precisamos implementar a trait Writer para o tipo de erro personalizado AppResult, onde podemos decidir como exibir o erro:

#[async_trait]
impl Writer for AppError {
    async fn write(mut self, _req: &mut Request, depot: &mut Depot, res: &mut Response) {
        res.render(Text::Plain("I'm a error, hahaha!"));
    }
}

No Salvo, um Handler pode retornar um Result, desde que tanto o tipo Ok quanto o tipo Err no Result implementem a trait Writer.

Tratamento de Erros com anyhow

Dado o uso generalizado do anyhow, o Salvo fornece suporte embutido para anyhow::Error. Quando o recurso anyhow está habilitado, anyhow::Error implementa a trait Writer e é mapeado para 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 o recurso anyhow, habilite o recurso anyhow do Salvo em Cargo.toml:

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

Isso permite que suas funções handler retornem diretamente anyhow::Result<T>:

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

Erros frequentemente contêm informações sensíveis, que geralmente não devem ser visíveis para usuários comuns por motivos de segurança e privacidade. No entanto, se você for um desenvolvedor ou administrador do site, pode preferir que os erros sejam totalmente expostos, revelando as informações de erro mais precisas.

Como mostrado, no método write, podemos acessar referências a Request e Depot, tornando conveniente implementar a abordagem acima:

#[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("I'm a error, hahaha!"));
        }
    }
}

Exibindo Páginas de Erro

As páginas de erro embutidas do Salvo atendem aos requisitos na maioria dos casos, exibindo páginas Html, Json ou Xml com base no tipo de dados da solicitação. No entanto, há situações em que ainda se deseja exibir páginas de erro personalizadas.

Isso pode ser alcançado implementando um Catcher personalizado. Para instruções detalhadas, consulte a seção Catcher.