<h2>Por que Customizar Tipos de Erro em Rust?</h2>
<p>Em Rust, tratamento de erros é uma parte fundamental do design da linguagem. O tipo padrão <code>Result<T, E></code> força você a lidar com possíveis falhas explicitamente, o que evita bugs silenciosos. No entanto, usar apenas <code>String</code> ou tipos genéricos não captura informações semânticas sobre <em>o que</em> deu errado. Tipos de erro customizados permitem que você defina erros específicos do seu domínio, tornando o código mais legível, mantível e type-safe.</p>
<p>Quando você cria tipos de erro próprios, pode implementar conversões automáticas, adicionar contexto rico, e fazer pattern matching sobre falhas específicas. Isso transforma tratamento de erros de uma tarefa incômoda em uma ferramenta poderosa para comunicar falhas e recuperar-se delas de forma elegante.</p>
<h2>Criando Seu Primeiro Tipo de Erro Customizado</h2>
<h3>O Básico: Uma Enumeração com derive</h3>
<p>A forma mais direta de criar um tipo de erro customizado é usar uma enumeração. Aqui está um exemplo prático de uma biblioteca de operações matemáticas:</p>
<pre><code class="language-rust">#[derive(Debug)]
enum MathError {
DivisionByZero,
NegativeSqrt(f64),
InvalidInput(String),
}
impl std::fmt::Display for MathError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
MathError::DivisionByZero => write!(f, "Divisão por zero não é permitida"),
MathError::NegativeSqrt(n) => write!(f, "Não é possível tirar raiz quadrada de {}", n),
MathError::InvalidInput(msg) => write!(f, "Entrada inválida: {}", msg),
}
}
}
impl std::error::Error for MathError {}
fn divide(a: f64, b: f64) -> Result<f64, MathError> {
if b == 0.0 {
Err(MathError::DivisionByZero)
} else {
Ok(a / b)
}
}
fn square_root(n: f64) -> Result<f64, MathError> {
if n < 0.0 {
Err(MathError::NegativeSqrt(n))
} else {
Ok(n.sqrt())
}
}</code></pre>
<p>Note que implementamos <code>Display</code> e <code>Error</code>. O trait <code>Display</code> é obrigatório para qualquer tipo que implemente <code>Error</code>. Isso permite que erros sejam convertidos em strings legíveis. O derive <code>#[derive(Debug)]</code> já vem pronto — é essencial para debugging.</p>
<h3>Usando o Operador <code>?</code> com Seus Erros</h3>
<p>O operador <code>?</code> torna o código conciso. Quando você retorna <code>?</code> em uma função que retorna um <code>Result</code>, o erro é automaticamente convertido e propagado:</p>
<pre><code class="language-rust">fn calculate(a: f64, b: f64) -> Result<f64, MathError> {
let quotient = divide(a, b)?;
let result = square_root(quotient)?;
Ok(result)
}
fn main() {
match calculate(16.0, 2.0) {
Ok(value) => println!("Resultado: {}", value),
Err(e) => println!("Erro: {}", e),
}
}</code></pre>
<h2>Conversão Automática Entre Tipos de Erro</h2>
<h3>O Trait <code>From</code> para Conversão Implícita</h3>
<p>Quando sua aplicação envolve múltiplos tipos de erro (seus erros customizados + erros de I/O, parsing, etc.), você precisa de conversão automática. O trait <code>From</code> permite que o operador <code>?</code> converta erros transparentemente:</p>
<pre><code class="language-rust">use std::io;
use std::num::ParseIntError;
#[derive(Debug)]
enum AppError {
Math(MathError),
Io(io::Error),
ParseInt(ParseIntError),
}
impl std::fmt::Display for AppError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
AppError::Math(e) => write!(f, "Erro matemático: {}", e),
AppError::Io(e) => write!(f, "Erro de I/O: {}", e),
AppError::ParseInt(e) => write!(f, "Erro ao fazer parsing: {}", e),
}
}
}
impl std::error::Error for AppError {}
impl From<MathError> for AppError {
fn from(err: MathError) -> Self {
AppError::Math(err)
}
}
impl From<io::Error> for AppError {
fn from(err: io::Error) -> Self {
AppError::Io(err)
}
}
impl From<ParseIntError> for AppError {
fn from(err: ParseIntError) -> Self {
AppError::ParseInt(err)
}
}
fn read_and_calculate(path: &str, divisor: &str) -> Result<f64, AppError> {
let content = std::fs::read_to_string(path)?;
let divisor_num: f64 = divisor.parse()?;
let value: f64 = content.trim().parse()?;
divide(value, divisor_num).map_err(AppError::Math)
}</code></pre>
<p>Note que implementamos <code>From</code> para cada tipo de erro externo. Agora, quando você usa <code>?</code> dentro de <code>read_and_calculate</code>, qualquer erro é automaticamente convertido para <code>AppError</code>.</p>
<h2>Boas Práticas e Padrões Avançados</h2>
<h3>Adicionar Contexto e Stack Trace</h3>
<p>Erros complexos frequentemente precisam de mais contexto. Uma abordagem prática é armazenar a fonte do erro:</p>
<pre><code class="language-rust">#[derive(Debug)]
struct DetailedMathError {
kind: MathErrorKind,
source: Box<dyn std::error::Error + Send + Sync>,
}
#[derive(Debug)]
enum MathErrorKind {
DivisionByZero,
NegativeSqrt,
}
impl DetailedMathError {
fn new(kind: MathErrorKind, source: Box<dyn std::error::Error + Send + Sync>) -> Self {
DetailedMathError { kind, source }
}
}
impl std::fmt::Display for DetailedMathError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{:?}: {}", self.kind, self.source)
}
}
impl std::error::Error for DetailedMathError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(self.source.as_ref())
}
}</code></pre>
<h3>Usar Bibliotecas Especializadas</h3>
<p>Para projetos maiores, considere usar <code>thiserror</code> ou <code>anyhow</code>. A crate <code>thiserror</code> reduz boilerplate drasticamente:</p>
<pre><code class="language-rust">use thiserror::Error;
#[derive(Error, Debug)]
enum MathError {
#[error("Divisão por zero não é permitida")]
DivisionByZero,
#[error("Não é possível tirar raiz de número negativo: {0}")]
NegativeSqrt(f64),
#[error("Entrada inválida: {0}")]
InvalidInput(String),
}</code></pre>
<p>Essa abordagem implementa automaticamente <code>Display</code> e <code>Error</code>. O arquivo <code>Cargo.toml</code> precisa incluir: <code>thiserror = "1.0"</code>.</p>
<h2>Conclusão</h2>
<p>Dominar tipos de erro customizados em Rust é essencial para escrever código robusto e idiomático. Os pontos principais são: <strong>(1)</strong> sempre implemente <code>Display</code> e <code>Error</code> para seus tipos customizados — é a base para integração com o resto do Rust; <strong>(2)</strong> use <code>From</code> para conversão automática entre tipos de erro, permitindo que o operador <code>?</code> funcione sem verbosidade; <strong>(3)</strong> para projetos maiores, prefira crates como <code>thiserror</code> para reduzir boilerplate e melhorar manutenibilidade. Com essas ferramentas, você transforma tratamento de erros de um necessário incômodo em um componente elegante da arquitetura da sua aplicação.</p>
<h2>Referências</h2>
<ul>
<li><a href="https://doc.rust-lang.org/book/ch09-00-error-handling.html" target="_blank" rel="noopener noreferrer">The Rust Book - Error Handling</a></li>
<li><a href="https://doc.rust-lang.org/rust-by-example/error.html" target="_blank" rel="noopener noreferrer">Rust by Example - Error Handling</a></li>
<li><a href="https://docs.rs/thiserror/latest/thiserror/" target="_blank" rel="noopener noreferrer">thiserror crate</a></li>
<li><a href="https://docs.rs/anyhow/latest/anyhow/" target="_blank" rel="noopener noreferrer">anyhow crate</a></li>
<li><a href="https://rust-lang.github.io/api-guidelines/type-safety.html" target="_blank" rel="noopener noreferrer">Rust API Guidelines - Error Types</a></li>
</ul>