Pular para o conteúdo

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.

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.

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 insuficiente
▶ Experimente você mesmo

Agora 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 insuficiente
▶ Experimente você mesmo

Agora 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.

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.

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 vazio
Erro: a senha precisa ter pelo menos 8 caracteres
▶ Experimente você mesmo

As 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.

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úmero
Erro: 250 é mais do que 100
▶ Experimente você mesmo
  • Um erro do tipo String funciona em exemplos pequenos, mas quem chama não consegue fazer match para distinguir cada caso.
  • Defina um enum com uma variante para cada tipo de falha, para que quem chama possa fazer match no 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 thiserror e anyhow eliminam boa parte do código repetitivo.

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