Rust

Guia Completo de Criando Tipos de Erro Customizados em Rust

8 min de leitura

Guia Completo de Criando Tipos de Erro Customizados em Rust

Por que Customizar Tipos de Erro em Rust? Em Rust, tratamento de erros é uma parte fundamental do design da linguagem. O tipo padrão força você a lidar com possíveis falhas explicitamente, o que evita bugs silenciosos. No entanto, usar apenas ou tipos genéricos não captura informações semânticas sobre o que 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. 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. Criando Seu Primeiro Tipo de Erro Customizado O Básico: Uma Enumeração com derive 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: Conversão Automática Entre Tipos

<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&lt;T, E&gt;</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(&amp;self, f: &amp;mut std::fmt::Formatter) -&gt; std::fmt::Result {

match self {

MathError::DivisionByZero =&gt; write!(f, &quot;Divisão por zero não é permitida&quot;),

MathError::NegativeSqrt(n) =&gt; write!(f, &quot;Não é possível tirar raiz quadrada de {}&quot;, n),

MathError::InvalidInput(msg) =&gt; write!(f, &quot;Entrada inválida: {}&quot;, msg),

}

}

}

impl std::error::Error for MathError {}

fn divide(a: f64, b: f64) -&gt; Result&lt;f64, MathError&gt; {

if b == 0.0 {

Err(MathError::DivisionByZero)

} else {

Ok(a / b)

}

}

fn square_root(n: f64) -&gt; Result&lt;f64, MathError&gt; {

if n &lt; 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) -&gt; Result&lt;f64, MathError&gt; {

let quotient = divide(a, b)?;

let result = square_root(quotient)?;

Ok(result)

}

fn main() {

match calculate(16.0, 2.0) {

Ok(value) =&gt; println!(&quot;Resultado: {}&quot;, value),

Err(e) =&gt; println!(&quot;Erro: {}&quot;, 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(&amp;self, f: &amp;mut std::fmt::Formatter) -&gt; std::fmt::Result {

match self {

AppError::Math(e) =&gt; write!(f, &quot;Erro matemático: {}&quot;, e),

AppError::Io(e) =&gt; write!(f, &quot;Erro de I/O: {}&quot;, e),

AppError::ParseInt(e) =&gt; write!(f, &quot;Erro ao fazer parsing: {}&quot;, e),

}

}

}

impl std::error::Error for AppError {}

impl From&lt;MathError&gt; for AppError {

fn from(err: MathError) -&gt; Self {

AppError::Math(err)

}

}

impl From&lt;io::Error&gt; for AppError {

fn from(err: io::Error) -&gt; Self {

AppError::Io(err)

}

}

impl From&lt;ParseIntError&gt; for AppError {

fn from(err: ParseIntError) -&gt; Self {

AppError::ParseInt(err)

}

}

fn read_and_calculate(path: &amp;str, divisor: &amp;str) -&gt; Result&lt;f64, AppError&gt; {

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&lt;dyn std::error::Error + Send + Sync&gt;,

}

#[derive(Debug)]

enum MathErrorKind {

DivisionByZero,

NegativeSqrt,

}

impl DetailedMathError {

fn new(kind: MathErrorKind, source: Box&lt;dyn std::error::Error + Send + Sync&gt;) -&gt; Self {

DetailedMathError { kind, source }

}

}

impl std::fmt::Display for DetailedMathError {

fn fmt(&amp;self, f: &amp;mut std::fmt::Formatter) -&gt; std::fmt::Result {

write!(f, &quot;{:?}: {}&quot;, self.kind, self.source)

}

}

impl std::error::Error for DetailedMathError {

fn source(&amp;self) -&gt; Option&lt;&amp;(dyn std::error::Error + &#039;static)&gt; {

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(&quot;Divisão por zero não é permitida&quot;)]

DivisionByZero,

#[error(&quot;Não é possível tirar raiz de número negativo: {0}&quot;)]

NegativeSqrt(f64),

#[error(&quot;Entrada inválida: {0}&quot;)]

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 = &quot;1.0&quot;</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>

Comentários

Mais em Rust

Boas Práticas de Serialização e Desserialização com Serde em Rust para Times Ágeis
Boas Práticas de Serialização e Desserialização com Serde em Rust para Times Ágeis

Introdução ao Serde: O Padrão de Serialização em Rust Serialização é o proces...

Guia Completo de Result<T, E> em Rust: Tratamento de Erros sem Exceções
Guia Completo de Result<T, E> em Rust: Tratamento de Erros sem Exceções

O Tipo Result em Rust Result é o tipo fundamental de Rust para tratamento de...

Como Usar Structs em Rust: Definição, Instanciação e Métodos em Produção
Como Usar Structs em Rust: Definição, Instanciação e Métodos em Produção

Definição e Conceitos Fundamentais Structs (estruturas) são um dos pilares da...