Erros personalizados em Rust
Na página de Result, todo erro era só uma String. Isso funciona bem em exemplos pequenos, mas deixa de funcionar quando uma função pode falhar de mais de um jeito diferente. Não dá para fazer match em uma String, então quem chama a função não consegue saber qual tipo de falha aconteceu sem analisar o próprio texto.
O problema dos erros do tipo String
Seção intitulada “O problema dos erros do tipo String”fn withdraw(balance: f64, amount: f64) -> Result<f64, String> { if amount > balance { return Err(String::from("saldo insuficiente")); } if amount < 0.0 { return Err(String::from("o valor não pode ser negativo")); } Ok(balance - amount)}Se quem chama quiser reagir de um jeito para “saldo insuficiente” e de outro para “valor negativo”, a única opção é comparar o próprio texto do erro. Isso é frágil e quebra no momento em que alguém muda a redação da mensagem.
Definindo um erro como enum
Seção intitulada “Definindo um erro como enum”Um enum resolve isso: cada tipo de falha vira uma variante própria, e quem chama pode fazer match diretamente nela:
enum WithdrawError { InsufficientFunds, NegativeAmount,}
fn withdraw(balance: f64, amount: f64) -> Result<f64, WithdrawError> { if amount < 0.0 { return Err(WithdrawError::NegativeAmount); } if amount > balance { return Err(WithdrawError::InsufficientFunds); } Ok(balance - amount)}
fn main() { match withdraw(100.0, 150.0) { Ok(remaining) => println!("Novo saldo: {remaining}"), Err(WithdrawError::InsufficientFunds) => println!("Dinheiro insuficiente"), Err(WithdrawError::NegativeAmount) => println!("Não é possível sacar um valor negativo"), }}Saída:
Dinheiro insuficienteAgora o compilador garante que todo tipo de falha seja tratado. É a mesma garantia que o match sempre deu com enums, só que aplicada a erros.
Implementando Display para ter uma boa mensagem de erro
Seção intitulada “Implementando Display para ter uma boa mensagem de erro”Se você tentar imprimir WithdrawError com {} agora, o código não compila: o Rust não sabe como transformá-lo em texto. Implementar o trait Display resolve isso. Os traits vão ganhar páginas próprias daqui a pouco (Traits e Traits comuns). Por enquanto, você pode copiar este padrão do jeito que está:
use std::fmt;
enum WithdrawError { InsufficientFunds, NegativeAmount,}
impl fmt::Display for WithdrawError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { WithdrawError::InsufficientFunds => write!(f, "saldo insuficiente"), WithdrawError::NegativeAmount => write!(f, "o valor não pode ser negativo"), } }}
fn main() { let error = WithdrawError::InsufficientFunds; println!("Erro: {error}");}Saída:
Erro: saldo insuficienteAgora WithdrawError se comporta como qualquer outro erro que você queira mostrar para um usuário ou gravar em um log, e continua sendo possível fazer match nele no código que precisa reagir a um caso específico.
Quando um crate pode ajudar
Seção intitulada “Quando um crate pode ajudar”Escrever o Display e todo o código repetitivo em volta de um tipo de erro personalizado fica cansativo em um projeto de verdade. O crate thiserror gera a maior parte disso para você com um #[derive], e o anyhow é popular em aplicações que só precisam propagar erros sem definir um tipo para cada um. Vale saber que eles existem, mas você não precisa deles para aprender os fundamentos. Quando estiver pronto, a página de anyhow e thiserror mostra os dois em ação e quando escolher cada um.
Prática
Seção intitulada “Prática”1. Erros de login
Seção intitulada “1. Erros de login”Defina um enum LoginError com duas variantes, EmptyUsername e PasswordTooShort, e implemente Display para ele. Depois, escreva fn check_login(user: &str, password: &str) -> Result<String, LoginError>, que devolve Ok com uma mensagem de boas-vindas ou o erro certo. A senha precisa ter pelo menos 8 caracteres. Dica: .is_empty() e .len() em um &str dão tudo o que você precisa para conferir.
Ver solução
use std::fmt;
enum LoginError { EmptyUsername, PasswordTooShort,}
impl fmt::Display for LoginError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { LoginError::EmptyUsername => write!(f, "o nome de usuário não pode ficar vazio"), LoginError::PasswordTooShort => write!(f, "a senha precisa ter pelo menos 8 caracteres"), } }}
fn check_login(user: &str, password: &str) -> Result<String, LoginError> { if user.is_empty() { return Err(LoginError::EmptyUsername); } if password.len() < 8 { return Err(LoginError::PasswordTooShort); } Ok(format!("Boas-vindas, {user}!"))}
fn main() { let attempts = [("ana", "cavalo-correto"), ("", "segredo123"), ("beto", "curta")];
for (user, password) in attempts { match check_login(user, password) { Ok(message) => println!("{message}"), Err(e) => println!("Erro: {e}"), } }}Saída:
Boas-vindas, ana!Erro: o nome de usuário não pode ficar vazioErro: a senha precisa ter pelo menos 8 caracteresAs senhas de exemplo não têm acentos de propósito: .len() conta bytes, e uma letra acentuada ocupa 2 bytes em UTF-8, o que mudaria a contagem.
2. Erros que trazem detalhes
Seção intitulada “2. Erros que trazem detalhes”Escreva fn parse_percent(input: &str) -> Result<u32, PercentError>, em que PercentError tem duas variantes que guardam dados: NotANumber(String) (o texto inválido) e TooLarge(u32) (o número que passou de 100). Faça as mensagens de Display incluírem esses detalhes. Dica: variantes de enum podem guardar valores, como na página de Enums. No fmt, faça o match com PercentError::TooLarge(n) => ... para tirar o valor de dentro.
Ver solução
use std::fmt;
enum PercentError { NotANumber(String), TooLarge(u32),}
impl fmt::Display for PercentError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { PercentError::NotANumber(text) => write!(f, "'{text}' não é um número"), PercentError::TooLarge(n) => write!(f, "{n} é mais do que 100"), } }}
fn parse_percent(input: &str) -> Result<u32, PercentError> { match input.parse::<u32>() { Ok(n) => { if n > 100 { Err(PercentError::TooLarge(n)) } else { Ok(n) } } Err(_) => Err(PercentError::NotANumber(input.to_string())), }}
fn main() { for input in ["75", "metade", "250"] { match parse_percent(input) { Ok(n) => println!("{n}%"), Err(e) => println!("Erro: {e}"), } }}Saída:
75%Erro: 'metade' não é um númeroErro: 250 é mais do que 100- Um erro do tipo
Stringfunciona em exemplos pequenos, mas quem chama não consegue fazermatchpara distinguir cada caso. - Defina um enum com uma variante para cada tipo de falha, para que quem chama possa fazer
matchno erro específico. - Implemente
Display(impl fmt::Display for SeuErro) para que o tipo de erro possa ser impresso com{}. - Em projetos maiores, crates como
thiserroreanyhoweliminam boa parte do código repetitivo.
Teste o que aprendeu
Seção intitulada “Teste o que aprendeu”Teste o que aprendeu
1. Por que um erro do tipo String deixa a desejar quando uma função pode falhar de mais de um jeito?
2. O que permite que quem chama withdraw reaja de forma diferente a InsufficientFunds e a NegativeAmount?
3. O que implementar Display para um tipo de erro personalizado permite fazer?
4. Em que crates como thiserror e anyhow ajudam?
5. Por que comparar o texto das mensagens de erro é considerado frágil?
Pontuação: 0 / 5
Para saber mais
Seção intitulada “Para saber mais”- Defining an error type — Rust by Example (em inglês): um exemplo curto, que você pode rodar, de um tipo de erro personalizado com
Display. - Wrapping errors — Rust by Example (em inglês): o passo seguinte, um enum de erro que envolve outros erros e funciona com
?através deFrom. std::error::Error— documentação da std (em inglês): o trait padrão que os tipos de erro “de verdade” implementam, e para que serve o métodosource().- To panic! or Not to panic! — The Rust Book (em inglês): orientações sobre quando devolver um erro e quando parar o programa.
- thiserror — docs.rs (em inglês): o crate que deriva
DisplayeErrorpara os seus enums de erro, para você escrever muito menos código.