WEB3DEV

Cover image for Erros em Rust: uma Fórmula
Isabela Curado Nehme
Isabela Curado Nehme

Posted on

Erros em Rust: uma Fórmula

https://miro.medium.com/v2/resize:fit:720/format:webp/0*2HSX1Lw_3Zz9cYz8.png

Os erros são um bolo de múltiplas camadas. Erros em Rust têm um fator de sabor porque se enquadram no campo de linguagem “há mais de uma maneira de fazer isso”. Mistura aprendizado de máquina funcional (como em: Ocaml), sabores tipo algoritmos para suas construções e pega as melhores ideias de outras linguagens. Tem opiniões rígidas sobre tudo que é segurança: simultaneidade, acesso à memória, compartilhamento de recursos e mais.

É por isso que o tratamento de erros em Rust é um bolo de múltiplas camadas.

→ → →
🙋‍♀️ Tem dúvidas? Quer compartilhar sua experiência? Siga-me no Twitter.😎

Observação: se você estiver impaciente, pode pular para o final para revisar a fórmula/lista de verificação de erros em Rust.

Escolhendo a Filosofia Correta de Tratamento de Erros

A título de comparação, observe o Go, que adota uma abordagem indiscutivelmente correta e diz: “Um erro é uma coisa simples. Lide com isso no local de chamada ou vá a falência”.

E observe o Java, que diz “Jogue-o e outra pessoa cuidará disso” o que, como a história prova, pode estar muito errado, muitas vezes.

O problema é que quando os erros são considerados algo simples como no Go, eles geralmente são uma string ou um objeto que, no momento em que chega ao manipulador, tem falta de contexto ou informações para tomar uma decisão inteligente de recuperação de erros. E com a abordagem do Java, onde um erro é lançado e o stack é destruído, adicione uma boa medida de falta de práticas recomendadas e, para cada camada gerada, você perde mais e mais contexto e, novamente - no momento em que o manipulador recebe um erro, ele está em uma posição ruim para tomar uma decisão informada.

Uma Filosofia de Erro no Rust

Os princípios básicos dos erros no Rust são estes:

// define um tipo Result como uma conveniência, já codificando nosso tipo Error 
type Result<T> = Result<T, ParserError>;

// um Error é um tipo, nada especial
#[derive(Debug)]
enum ParserError {
    EmptyText,
    Parse(String),
}
// retorna um result, que pode ou não ser um erro. Para encontrá-lo, faça um `match` em seu conteúdo
fn parse(..) -> Result<AST>{
  ..
}
Enter fullscreen mode Exit fullscreen mode

Temos um tipo Result, que codifica os estados Ok e Err, que são representados como tipos que você pode criar. O tipo de erro pode ser qualquer coisa, mas um enum é uma ideia inteligente (e a mais popular).

Uma função retorna um Result.

Para gerenciar erros, o Rust identifica as lacunas de outras linguagens e diz:

  • Lide com erros no local de chamada (callsite) ou
  • Gere erros de maneira ordenada (o que também significa: nenhum mecanismo de throw/catch como em Java).

Para lidar com erros, o Rust também oferece tudo que você precisa, como parte da linguagem. O tratamento de erros é de primeira classe:

  • Os erros são incentivados a serem tipados com riqueza de detalhes (por exemplo, ao contrário do Go).
  • Você é incentivado a fazer match de erros, e o Rust reconhece que erros e falhas são uma experiência rica, que pode facilmente se tornar frustrante e oferece ergonomia para situações não tão triviais de tratamento de erros. Mais uma vez, ao contrário do Go.
  • Passar erros é semelhante a retornar resultados de funções, que é o que uma linguagem de programação já sabe fazer. Como o Go, mas diferente do Java. Isso significa que o programador não deve ser pego de surpresa e que cada construção poderosa que um programador precisa para criar ou manipular resultados de funções (agregação, construção de expressões, conversão, simultaneidade, etc.) está disponível para lidar com erros, ao contrário do Go e ao contrário do Java.

Como um erro é apenas um valor de retorno, você pode modelar isso em qualquer outra linguagem de programação, certo?

Posso modelar erros como tipos result em Java, qual é o problema? E o grande problema é como tudo se conecta - quando a biblioteca padrão está toda orientada para isso, a sintaxe considera os erros como resultados, e a comunidade e a mentalidade já estão lá, assim como você obtém o ápice da história de erros.

<rant> Erros Não São Simples </rant>

Embora isso não seja para desmerecer o Go, a diferença entre o Go e o Rust é uma ótima ilustração de como o “pensamento da linguagem” (por exemplo, o teorema de Wharf) afeta o código e a carga cognitiva.

Todas as pessoas que migraram de Go para Rust que conheci me disseram que não conseguem voltar atrás. Que não conseguem acreditar como calcularam mal a complexidade dos erros e que hoje conseguem ver a quantidade de casos extremos que seu código Go ignorava.

É tudo uma questão de transferência de peso de carga cognitiva: em C, os erros são simples porque C era um pré-processador de Assembly. É por isso que a complexidade é transferida para o desenvolvedor. Em Go, que visa melhorar esse aspecto de forma clássica, mas ainda se inspira na simplicidade, o peso também é transferido para o desenvolvedor. A diferença é que em C você sabia que não tinha rede de segurança.

É por isso que eles não conseguiam acreditar como estavam “vendidos” na ideia de que os erros são “unidimensionais”, simples, (por exemplo, lidar com eles ou simplesmente falhar, usando o típico reflexo muscular if err != nil que você obtém no Go). Falhas não são simples. Casos de erro não são triviais. É por isso que temos naves espaciais caindo, pessoal.

Não Existe Almoço Grátis

Erros em Rust são uma experiência de aprendizado e uma ferramenta poderosa. Erros não são aquela coisa incômoda com a qual você precisa lidar, e um mestre em erros é um mestre na linguagem.

O Rust oferece ferramentas para construir uma ótima história de modelagem e tratamento de erros que pode potencializar um software de missão crítica - se essa for sua preferência. Para mim, embora eu não construa softwares para naves espaciais e reatores nucleares, sempre gostei de construir softwares confiáveis que não te acordem à noite.

É por isso que os erros no Rust exigem mais investimento de sua parte.

  • Criar erros tem mais carga cognitiva. Você não pode simplesmente lançar um novo erro new Error("oops") ou retornar "can't load" ou throw new Foobar(). Os erros precisam ser criados como um tipo e obedecer a algumas regras.
  • Conectar-se com uma história de falha: para fornecer backtrace, causa raiz.
  • Conectar-se com uma experiência do usuário: forneça uma implementação de depuração e visualização que satisfaça o operador e ao usuário final, respectivamente.
  • Converter os resultados de maneira sensata e responsável: você perceberá rapidamente que o retorno de uma função Result<(), ErrA> não funciona com o retorno de uma função interna Result<(), ErrB>.
  • Lidar com a complexidade de um sistema real, onde se pode obter um erro de várias camadas: Error(DatabaseError(ConnectionError(PoolError(reason)))), cada camada dessa cebola de erro tem uma bifurcação e uma decisão que você precisa codificar explicitamente. Isso é uma coisa boa, a menos que o autor da biblioteca tenha feito um péssimo trabalho de modelagem de erros; nesse caso, você está apenas compensando suas lacunas, o que também é uma ação de prevenção de bugs, em qualquer caso.
  • Ser responsável com os outros. Os erros que você retorna podem ser consumidos por outras pessoas. Você precisa pensar com intenção sobre que tipo de história de erro eles terão.

Estrutura, Semântica e Experiência do Usuário

Analisaremos alguns padrões práticos e desafios de design. Tudo aqui será construído com apenas duas bibliotecas de erros.

  1. [thiserror](https://crates.io/crates/thiserror) - para bibliotecas e núcleo;
  2. [eyre](https://crates.io/crates/eyre) (que é uma alternativa semelhante/intermediária à anyhow) - opcional, se você estiver visando aplicativos (principalmente CLI).

Analisaremos:

  • Como criar e adicionar contexto aos nossos próprios erros;
  • Como colocar em camadas, aninhar e empacotar erros;
  • Mapear erros de dependências em nossos próprios erros em um relação 1:1, 1:N, N:1.

Criação de Erros

Ao manter um formulário enum, é possível criar uma hierarquia de erros que mantém o contexto:

enum ParserError {
 EmptyFile(String) // o caminho do arquivo
 Json(serde_json::Error) // o erro original do analisador JSON 
 SyntaxError { line: String, pos: usize } // uma complexa estrutura de erro
}
Enter fullscreen mode Exit fullscreen mode

Quando queremos criar erros ou devolver erros, queremos manter os seguintes princípios:

  • Digitar menos: use conversões From extensivamente e automáticas usando ?.
  • Usar um loop “for-in” em vez de mapeamento para curto-circuito.
  • Coletar Result<Vec> para agregação, em vez de Vec>.
let res: Result<Vec<_>> = foo.map(..).collect();
Enter fullscreen mode Exit fullscreen mode
  • Manter os erros de origem sempre que possível.
  • Em Error, tanto na visualização quanto na depuração são importantes: o público. A visualização se destina aos usuários finais (por exemplo, redigir segredos, tornar um texto longo mais curto) e a depuração destina-se aos operadores (por exemplo, resolução de problemas, registro e diagnóstico).
  • Sem expect, unwrap e panic no seu código, a menos que seja obrigatório (e principalmente, devido a uma lacuna em uma das bibliotecas externas).

Erros a Nível do Crate e a Nível do Módulo

Embora você possa definitivamente ter um único tipo Result, com um único tipo Error para todo o seu crate, muitas vezes você pode se beneficiar da subdivisão do seu tipo Error em sub-erros relevantes para seus módulos de crate individuais. Ao usar um único repositório com espaço de trabalho com múltiplos crates, isso é inevitável.

O significado disso é que você também subdividirá seu tipo Result único em muitos tipos Result diferentes relevantes para seus sub-módulos.

Por exemplo:

compiler/      -> (1) Result<T,E> = Result<.., Error>
  parser/      -> (2) Result<T,E> = Result<.., ParserError>
  scanner/     -> (3) Result<T,E> = Result<.., ScannerError>
Enter fullscreen mode Exit fullscreen mode

O tipo de erro a nível do crate é (1).

O tipo de erro a nível do módulo são (2) e (3).

Os três tipos de Result diferentes são tipos diferentes. Por isso, ao retornar Result<.., ParserError> a uma função compiler que espera Result<.., Error>, seu código vai se quebrar.

E assim, você precisará de .map_err, se o Result T for o mesmo, ou empacotar o seu Result enquanto passa o valor para cima a partir das dependências downstream (parser ou analisador) para as dependências upstream (compiler ou compilador).

Mapeamento de Erros

Às vezes, lidar com erros é simplesmente mapeá-los para um tipo de erro diferente, o que significa separar o erro original, extrair algum contexto dele ou empacotá-lo como está em um tipo de erro diferente.

Isso requer um erro de base bem pensado para começar. Acho que, na maioria das vezes, é ótimo ter isso como ponto de partida (e costumo copiá-lo para cada novo módulo que crio).

#[derive(thiserror::Error, Debug)]
pub enum Error {
   #[error("{0}")]
   Message(String),
   // um empacotador IO central
   #[error(transparent)]
   IO(#[from] std::io::Error),
   // será usado com `.map_err(Box::from)?;`
   #[error(transparent)]
   Any(#[from] Box<dyn std::error::Error + Send + Sync>),


   // algumas outras conversões comuns
   #[error(transparent)]
   Pattern(#[from] regex::Error),
   #[error(transparent)]
   Json(#[from] serde_json::Error),
}
Enter fullscreen mode Exit fullscreen mode

Empacotar serde_json::Error em Error::Json é como repetir informações existentes e não agregar nenhum valor. Estamos apenas repetindo coisas?

Bem, há duas coisas acontecendo aqui:

  1. A consolidação de diferentes tipos de erros, possivelmente de diferentes bibliotecas, como um tipo de erro enum unificado, o que simplifica os tipos Result significativamente - isso é fundamental para o tratamento útil de erros de sua biblioteca por terceiros. Ao usar uma biblioteca, é melhor esperar apenas um tipo de erro que informe sobre todos os erros possíveis nesta biblioteca.
  2. O desfrute de conversões automáticas com #[from] para limpar o seu código, economizar digitação e economizar manutenção, evitando o ponto de decisão de conversão de erro. Você sempre pode ir "manualmente" e remover #[from], e fazer você mesmo a conversão em cada ponto de código da sua base de código.
  3. A preservação de uma saída de emergência para aqueles momentos de anyhow. Não se importa com um erro? Não sabe o que fazer com isso? Os autores da biblioteca tornaram impossível trabalhar? Faça este golpe de caratê: .map_err(Box::from)?; e empacote-o com seu próprio tipo Error acessível.

Mapeamento: N:1

Mapear muitos tipos de erros de terceiros para um de seus tipos de erro: quando você deseja fazer isso?

  • Quando uma ou mais bibliotecas têm um nível muito preciso de detalhes em seus erros. Por exemplo, quando Error::HttpThrottling, Error::RateLimit e Error::AccountDepleated são todos iguais, indicando que: "você está sem seus créditos de API ou está abusando de seus créditos - se acalme!".
  • Quando você já sabe que o jogo acabou. Conhecer as especificidades do erro não ajudará no seu serviço. Por exemplo Error::DiskFull, Error::CorruptPartition, etc. Basta empacotá-los em um MyError::Fatal e manter o erro original nele, encaixotado, para obter mais detalhes.
  • Quando várias bibliotecas diferentes estão fazendo a mesma coisa e implementam uma arquitetura de provedor onde você pode trocar diferentes provedores implementando um trait. Por exemplo, um Error::PostgresConnection, Error::MySqlConnectionPool, com um provedor de banco de dados intercambiável em seu crate pode significar apenas um MyError::Connection para você. Lembre-se de que se o trait precisa ser universal em relação aos provedores, os erros retornados nas funções de trait também devem ser.

Há duas maneiras de criar esse tipo de mapeamento.

1. Mapeamento N:1 – Coloque em Camadas e Crie

Essencialmente, queremos criar um tipo de erro agregado de primeiro nível e um agregado de erros de nível superior.

Digamos que você tenha provedores de banco de dados e, em seguida, tenha seu crate que acessa os dados.

Primeiro, crie um tipo de erro de primeiro nível:

enum DbProviderError {
  // todos estes são invariavelmente comuns a todos os 
  // provedores de banco de dados com os quais você está lidando
  Connection(..)
  PoolLimit(..)
  SqlSyntax(..)
}
Enter fullscreen mode Exit fullscreen mode

Ter um trait para esses provedores retornará o erro acima, para alinhar todas as especificidades de cada erro diferente dos diferentes provedores:

trait DbProvider {
 fn connect(..)  ->  Result<(), DbProviderError>
}
Enter fullscreen mode Exit fullscreen mode

Finalmente, seu crate que usa DbProvider, configura o provedor certo, etc., precisa ser capaz de aceitar DbProviderError:

enum MyError {
    //..
    Message(String)
    #[error(transparent)]
    DB(#[from] DbProviderError),
}
Enter fullscreen mode Exit fullscreen mode

Agora, usar ? para converter um erro de banco de dados em um erro a nível do crate deve criar uma história de erro bonita e organizada.

NOTA: haverá casos em que os dois tipos “result” do seu trait e o seu tipo “result” a nível do crate não serão compatíveis e a conversão através de ? não funcionará. É aí que você terá que chamar manualmente .into() em um DbProviderError para transformá-lo em um MyError ao fazer um .map_err ou criar um novo DbProviderError você mesmo.

Um exemplo:

fn embed_inputs(&self, inputs: &[&str]) -> Result<Vec<Vec<f32>>> {
     Err(EmbeddingError::Other(
         "an embedding cannot be made".to_string(),
     )
     .into())
}
Enter fullscreen mode Exit fullscreen mode

Aqui, embed_inputs está em um módulo de incorporação (embedding) e tem sua própria hierarquia e história de erros, e seu próprio tipo Error.

Porém, ele retorna um Result a nível do crate mais alto e, embora contenha um Error a nível do crate que pode converter um EmbeddingError com um trait from, ele não pode ser inferido automaticamente, por isso estamos usando into() em nossa diretiva Err.

Outra maneira de converter manualmente é chamar o trait into diretamente e depois usar uma conversão ?:

fn embed_inputs(&self, inputs: &[&str]) -> Result<Vec<Vec<f32>>> {
  let res = provider
    .do_something()
    .map_err(Into::<..error type..>::into);
  Ok(res?)
}
Enter fullscreen mode Exit fullscreen mode

Uma observação sobre a estrutura de pastas e módulos

Ao lidar com provedores e traits de implementação de provedores, muitas vezes temos que mapear N tipos de erros e variantes em uma variante nossa.

Nesse caso, podemos criar uma arquitetura de erros de aparência semelhante, onde cada camada conhece seus próprios erros concretos.

root/
  error.rs
  providers/
    error.rs
    providerA/
      error.rs
    providerB/
      error.rs
    ...
Enter fullscreen mode Exit fullscreen mode

E o próximo passo é oferecer tipos de erros encapsulados e conversões locais de módulo:

root/
  error.rs
    providers/
    error.rs
      .. {
        ProviderA(provider_a::error::ProviderAError)
        ProviderB
      }
    providerA/
    error.rs
      .. {
        SqlConnectionError(extlib::conn:Error)
        DataTransferError(extlib::conn:Error)
      }
    providerB/
      error.rs
    ...
Enter fullscreen mode Exit fullscreen mode

Mas, muitas vezes, queremos “agrupar” erros do provedor sem nos importar com os detalhes dentro de cada um desses erros. Como é um nível muito baixo e um usuário não consegue lidar com eles, temos apenas um erro de provedor. Em essência, estamos agrupando erros N a 1.

2. Mapeamento N:1 – Coloque em uma Caixa

Se você não se importa com o tipo de erro específico do provedor de banco de dados, você pode empacotá-lo, encaixotá-lo e enviá-lo com um erro a nível do crate MyError:

enum MyError {
    //..
    Message(String)
    DB(Box<dyn std::error::Error + Send + Sync>),
}
Enter fullscreen mode Exit fullscreen mode

E, em seguida, .map_err(Box::from).map_err(|e| MyError::DB(e)),. Observe que estamos sendo muito explícitos aqui com .map_err, para dar espaço para mais variantes que são Box<dyn Error> sob o tipo MyError.

Mapeamento 1:1

Mapeando um único erro de terceiros para uma de nossas próprias variantes de erro (para o propósito desta discussão, tratamos coisas como stdlib como sendo de terceiros também).

Isso nos permite mapear nossas variantes de erro, o que nos permite saltar pelas hierarquias de erros conforme elas existem em nossos módulos e crates.

Por exemplo, para subir através das camadas, visto como um conjunto de ações abstratas:

3rd-party error -> (wrap!) -> ModuleError -> (wrap!) -> CrateError
Enter fullscreen mode Exit fullscreen mode

Tomando um exemplo concreto de analisador, visto como uma árvore:

// crate
ParserError::Invalid(
  // módulo
  ScannerError::BadInput(
    // de terceiros
    Regex::Error(..)
  )
)
Enter fullscreen mode Exit fullscreen mode

Saltando pelas camadas do código:

fn parse(..) -> Result<String, ParserError> {
 scan()?; // erro de módulo -> erro de crate
}

fn scan(..) -> Result<String, ScannerError> {
 scan_with_regex()?; // erro de lib -> erro de módulo
}

fn scan_with_regex(..) -> Result<String, Regex::Error> {
  ...
}
Enter fullscreen mode Exit fullscreen mode

Você precisa de todas essas camadas? Muitas vezes não. Porém, entender essa estrutura básica de erros permitirá que você entenda outras bibliotecas e que seja capaz de “cortar” o que precisa desse quadro geral quando precisar.

Mapeamento 1:N

Isso acontece quando você recebe um tipo de erro e precisa de mais granularidade em seu próprio código. Um exemplo comum é um HTTPError que trata tudo como um erro, mas você sabe que um 404 é diferente de um 500, portanto, deseja estratégias diferentes de tratamento de erros.

O Rust faz um ótimo trabalho ao fornecer um .map_err exatamente para isso. E com uma cláusula match também se torna ergonomicamente agradável:

.map_err(|e| match e {
  // use e.code para criar suas variantes de erro
})
Enter fullscreen mode Exit fullscreen mode

Truques de Erro

Ad-hoc into

fn do_something(..) -> Result<String> {
  foobar(..).map_err(Into<ModuleError>::into)?;
  ...
}
Enter fullscreen mode Exit fullscreen mode

2-level From

Algumas vezes, é possível pular duas camadas de erros em cada local de chamada, mas, quando isso é feito várias vezes, é melhor refatorá-lo em um trait From. Isso limpa seu código e centraliza sua tomada de decisão sobre erros de forma eficiente.

impl From<lib error> for Error {
    fn from(e: <lib error>) -> Self {
        Self::SomeCrateError(super::ModuleError::SomeModuleError(...))
    }
}

// algum outro lugar
fn do_something(..) -> Result<String> {
  foobar(..)?;
  ...
}
Enter fullscreen mode Exit fullscreen mode

Quick box

Se você tiver esse tipo de erro:

#[derive(thiserror::Error, Debug)]
enum MyError {
   #[error("{0}")]
   Message(String)
   // observe esta variante
   #[error(transparent)]
   Any(#[from] Box<dyn std::error::Error + Send + Sync>),
}
Enter fullscreen mode Exit fullscreen mode

Então você pode fazer um rápido golpe de caratê para converter qualquer erro em um MyError::Any.

foo(..).map_err(Box::from)?;
Enter fullscreen mode Exit fullscreen mode

Se você usa anyhow, usar MyError::Any é uma boa alternativa para evitar ter que adicionar outra biblioteca.

Tratamento de Erros

https://miro.medium.com/v2/resize:fit:720/format:webp/0*Vintc_LNIWLgW-MM.png

Mach, Map, Wrap

Ao lidar com erros do crate do AWS SDK, queremos dizer que um parâmetro em falta em ssm está OK e não é um caso de erro quando excluímos um parâmetro:

fn handle_delete(e: SdkError<DeleteParameterError>, pm: &PathMap) -> Result<()> {
    match e.into_service_error() {
        DeleteParameterError::ParameterNotFound(_) => {
            // estamos ok
            Ok(())
        }
        e => Err(crate::Error::DeleteError {
            path: pm.path.to_string(),
            msg: e.to_string(),
        }),
    }
}
Enter fullscreen mode Exit fullscreen mode
  • Match (correspondência) – escolhe os vários casos de erro do tipo Error principal;
  • Map (mapeamento) — cria uma semântica diferente para o tipo Result original (mapeando um caso de erro para um caso Ok);
  • Wrap (empacotamento) - retorna um tipo simplificado e familiar de Error que nosso crate vai expor à capacidade de composição dos usuários finais.

Como regra geral, o tratamento de erros será sempre uma seleção de (1) correspondência, (2) mapeamento, (3) empacotamento ou todos eles combinados.

Tratamento de Saída

Em muitos casos, não há forma de se recuperar de um erro. Portanto, o melhor que você pode fazer é imprimi-lo e sinalizá-lo adequadamente (por exemplo, usando códigos de saída Unix apropriados).

Aqui está um exemplo de tal tratamento, onde um programa pode reportar um erro esperado como parte de um valor CmdResponse, que não é um Error do Rust, mas também, dado um erro inesperado com Error, irá reportá-lo de forma ordenada.

const DEFAULT_ERR_EXIT_CODE: i32 = 1;
pub fn result_exit(res: Result<CmdResponse>) {
    let exit_with = match res {
        Ok(cmd) => {
            if let Some(message) = cmd.message {
                if exitcode::is_success(cmd.code) {
                    eprintln!("{message}");
                } else {
                    eprintln!("{} {}", style("error:").red().bold(), style(message).red());
                };
            }
            cmd.code
        }
        Err(e) => {
            eprintln!("error: {e:?}");
            DEFAULT_ERR_EXIT_CODE
        }
    };
    exit(exit_with)
}
Enter fullscreen mode Exit fullscreen mode

Pense na Sua Área de Superfície

https://miro.medium.com/v2/resize:fit:720/format:webp/0*fOCyw92CZ7ZRakB5.png

Certifique-se de que seu crate exponha um único tipo de erro.

Por que?

  • Isso fará com que seus usuários possam criar uma única conversão from e pronto.
  • A documentação sobre o que tratar e como tratar está em um único lugar.
  • Para aqueles que desejam cobrir todos os casos, a correspondência de todas as variantes de um único enum Error faz essa função de forma fiel.
  • Foco em relatórios, depuração e operabilidade - enquanto trabalham com seu crate, os usuários conhecem intimamente um único tipo de erro e seu contexto e, assim, são capazes de lidar com ele de maneira eficaz, seja em uma sessão de depuração ou por meio do código.
  • Um tipo de erro significa uma espécie de tipo Result, que, por si só, é um design de API melhor.
  • Se necessário, aninhe outros tipos de erro dentro desse único tipo de erro em uma de suas variantes.

Erros: uma Fórmula

Siga estas etapas para obter o ápice dos erros em Rust.

1. Adicione e aprenda suas dependências:

  • thiserror para todos os erros em seu crate;
  • eyre para CLIs.

2. Crie um tipo de erro base por crate.

Você pode colocá-lo em seu nível superior mod.rs ou lib.rs.

// lib/mod.rs
#[derive(thiserror::Error, Debug)]
pub enum Error {
    #[error("{0}")]
    Message(String),
    // um empacotador IO central
    #[error(transparent)]
    IO(#[from] std::io::Error),
    // será usado com `.map_err(Box::from)?;`
    #[error(transparent)]
    Any(#[from] Box<dyn std::error::Error + Send + Sync>),


    // algumas outras conversões comuns
    #[error(transparent)]
    Pattern(#[from] regex::Error),
    #[error(transparent)]
    Json(#[from] serde_json::Error),
}
Enter fullscreen mode Exit fullscreen mode

3. Match, map, wrap:

Pegue erros das dependências, da stdlib ou de outros módulos, e quando necessário, faça corresponder (match) e extraia informações, ou mapeie-os com map_err, ou empacote-os em seu próprio tipo de erro.

4. No seu código, continue usando conversões ?.

Deixe o compilador ajudá-lo e faça conversões from automáticas.

Se um erro de uma biblioteca de terceiros não puder ser convertido automaticamente, adicione uma variante enum ao seu erro de crate de nível superior com o atributo #[from]. Verifique se você não está criando variantes concorrentes (o compilador informará você).

Ao tentar converter várias camadas de erros, codifique seu próprio trait From para ajudar a centralizar os pontos de tomada de decisão de erros.

impl From<RustBertError> for Error {
    fn from(e: RustBertError) -> Self {
Self::EmbeddingErr(super::EmbeddingError::SentenceEmbedding(Box::from(e)))
    }
}
Enter fullscreen mode Exit fullscreen mode

Lembre-se, você também pode utilizar .map_err em um erro que pode ser convertido por meio de ? e traits from.

5. Crie variantes contextuais, mas não exagere.

Crie variantes que contenham informações disponíveis quando um erro for criado.

InvalidSyntax{ file: String, line: usize, reason: String }
Enter fullscreen mode Exit fullscreen mode

Mas não exagere colocando todas as informações que existem no universo.

6. Pense na operacionalidade.

Pergunte a si mesmo:

  • Um usuário pode fazer algo para recuperar as informações que você está codificando com um erro que está criando?
  • Trata-se de uma recuperação automática? Ou manual?
  • Existe uma importância em termos de tempo? De espaço? De recursos? De hardware?
  • Os erros aparecerão nos registros? O que eles precisam conter?
  • Após uma falha, um erro fornecerá ao usuário informações suficientes para corrigir um problema?

7. Quando tudo mais falhar, use Box.

Use uma variante em seu erro que pode simplesmente receber um dyn Error se o erro de origem não for importante, mas você precisa criar uma base de código ergonômica.

🙋‍♀️ Tem dúvidas? Quer dizer oi? Siga-me no twitter.

Este artigo foi escrito por Dotan Nahum e traduzido por Isabela Curado Nehme. Seu original pode ser lido aqui.

Top comments (0)