A Filosofia de Tratamento de Erros do Rust
Rust não possui exceções no sentido que Java, Python ou JavaScript têm. Uma função que pode falhar declara isso em seu tipo de retorno, Result<T, E>, e o chamador é estaticamente obrigado a reconhecer essa possibilidade antes de acessar o valor de sucesso. Falha não é um canal de controle de fluxo especial e invisível que se desenrola silenciosamente por camadas de código que nunca optaram por participar - é um valor comum, presente na assinatura do tipo.
Esta é a maior mudança em como Rust trata erros em comparação com a maioria das linguagens populares. Result<T, E> e Option<T> não são recursos nativos da linguagem com sintaxe especial; são enums simples, os mesmos abordados em Modelando Dados com Structs e Enums, combinados exaustivamente como qualquer outro tipo soma. Uma vez que você vê erros como dados, o restante da maquinaria de tratamento de erros do Rust - ?, unwrap, tipos de erro personalizados - se torna uma consequência dessa única decisão, em vez de um monte de recursos separados para memorizar.
Esta página é a âncora conceitual para a seção. Noções Básicas de Tratamento de Erros aborda a sintaxe na prática; esta página explica a filosofia por trás dela.
Resumo
- Falha é representada como um valor comum -
Result<T, E>para erros recuperáveis,Option<T>para ausência - verificado exaustivamente pelo compilador em vez de propagado através de desenrolamento de exceção invisível. - Por que Importa: A assinatura de uma função informa aos chamadores se ela pode falhar antes que leiam uma única linha de sua implementação, e o compilador se recusa a permitir que um
Resultseja ignorado silenciosamente - eliminando uma categoria inteira de bugs do tipo "esquecemos de tratar o erro". - Conceitos Chave:
Result<T, E>,Option<T>, erros recuperáveis versus irrecuperáveis,panic!, o operador?, propagação de erros. - Quando Usar Este Modelo: Ao projetar qualquer função que possa falhar, decidir entre retornar
Resulte chamarpanic!, e ao ler código de biblioteca onde o tipo de erro em uma assinatura é a documentação primária do que pode dar errado. - Limitações / Compensações: Valores como erros significam que o tratamento de erros é explícito em todos os lugares, o que pode parecer verboso em comparação com
try/catch, especialmente antes do operador?e de bibliotecas comoanyhow/thiserrorsuavizarem a ergonomia. - Tópicos Relacionados: tipos de dados algébricos, o operador
?, tipos de erro personalizados, panics versus erros recuperáveis.
Fundamentos
Result<T, E> é definido como um enum com duas variantes: Ok(T) para sucesso, carregando o valor, e Err(E) para falha, carregando um valor de erro. Option<T> é seu irmão mais simples, Some(T) ou None, usado quando a ausência de um valor não é em si um erro que vale a pena descrever - uma busca que não encontra nada, em vez de uma busca que falhou.
fn parse_port(input: &str) -> Result<u16, std::num::ParseIntError> {
input.parse::<u16>()
}Como Result é um enum comum, você interage com ele da mesma forma que interage com qualquer outro enum: match, if let, ou um de uma grande família de métodos combinadores (.map, .unwrap_or, .and_then). Não há sintaxe try/catch separada para aprender, e nenhuma maneira de um Result ser silenciosamente descartado sem pelo menos ser vinculado a algo - #[must_use] em Result significa que o compilador avisa se você chamar uma função falível e nunca inspecionar o resultado.
Rust divide a falha em duas categorias genuinamente diferentes, e confundir essas categorias é o erro inicial mais comum. Erros recuperáveis são possibilidades esperadas às quais um chamador bem escrito pode responder de forma sensata - um arquivo que não existe, uma requisição de rede que expira, uma entrada que falha ao analisar. Erros irrecuperáveis são bugs: um invariante quebrado, um índice provado fora dos limites, um estado que o programa nunca deveria realmente alcançar. Erros recuperáveis usam Result; erros irrecuperáveis usam panic!, que desenrola (ou aborta, dependendo da configuração) a thread atual em vez de pedir ao chamador para lidar com algo.
Mecânicas e Interações
O operador ? é a peça que torna o tratamento de erros baseado em Result ergonômico em vez de tedioso, e vale a pena ser preciso sobre o que ele realmente faz: nada mais que um retorno antecipado. Colocado após uma expressão do tipo Result, ? desempacota Ok(value) em value e continua, ou, em Err(e), retorna imediatamente da função envolvente com Err(e.into()).
fn read_port(path: &str) -> Result<u16, Box<dyn std::error::Error>> {
let text = std::fs::read_to_string(path)?; // retorno antecipado em Err
let port = text.trim().parse::<u16>()?; // retorno antecipado em Err
Ok(port)
}Essa chamada .into() dentro de ? está fazendo um trabalho real: ela permite que uma função retorne um tipo de erro enquanto propaga vários tipos de erro subjacentes diferentes (um erro de I/O, depois um erro de análise, acima), desde que todos eles se convertam no tipo de erro declarado da função via From. É por isso que enums de erro personalizados frequentemente implementam From<std::io::Error> e conversões semelhantes - é o que permite que ? encadeie suavemente através de fontes de erro que, de outra forma, exigiriam encapsulamento manual em cada etapa.
Como ? é apenas açúcar sintático para um retorno antecipado, ele compõe com o fluxo de controle comum exatamente como você esperaria: ele pode aparecer dentro de ramos if, loops, closures retornando Result, em qualquer lugar onde uma expressão do tipo Result possa ser avaliada dentro de uma função cuja própria assinatura de retorno seja compatível. Ele não muda que tipo de tratamento de erro está acontecendo, apenas quanto código repetitivo é necessário para fazê-lo.
panic! funciona através de um mecanismo completamente diferente: ele não retorna nada a um chamador, porque não há um valor Err sendo passado de volta através do sistema de tipos. Por padrão, um panic desenrola a pilha, executando destrutores ao longo do caminho, e então termina a thread; uma configuração de compilação panic = "abort" pula o desenrolamento e termina o processo imediatamente em vez disso. De qualquer forma, panics não são destinados a serem rotineiramente capturados e tratados da maneira que exceções são frequentemente em outras linguagens - catch_unwind existe, mas alcançá-lo como uma estratégia geral de tratamento de erros geralmente sinaliza que a falha deveria ter sido um Result em primeiro lugar.
Considerações Avançadas e Aplicações
Linguagens diferentes resolvem o mesmo problema subjacente - como sinalizar e tratar falhas - com compensações significativamente diferentes.
| Abordagem | Força | Fraqueza | Melhor Ajuste |
|---|---|---|---|
Result/Option como valores (Rust) | Falha é visível na assinatura; compilador impõe o tratamento | Mais explícito em cada local de chamada; requer maquinaria de conversão (From, ?) para propagação ergonômica | Bibliotecas e código de sistema onde os chamadores precisam saber exatamente o que pode falhar |
| Exceções (Java, Python, JS) | Código de caminho feliz conciso; erros podem pular muitos quadros de pilha automaticamente | A assinatura de uma função não revela o que ela pode lançar; uma exceção não capturada surge longe de sua causa | Código de aplicação priorizando brevidade sobre garantias exaustivas em nível de assinatura |
| Códigos de erro (C) | Simples, nenhuma maquinaria de linguagem necessária | Trivial ignorar um código de retorno; nenhuma aplicação do compilador de qualquer tipo | Código de baixo nível ou embarcado sem suporte de sistema de tipos mais rico |
panic! (Rust, para bugs) | Falha alto e imediatamente no ponto de um invariante quebrado | Não é um caminho recuperável pelo chamador; ferramenta errada para condições de falha esperadas | Erros genuínos de programador e invariantes quebrados, não modos de falha de rotina |
A distinção biblioteca versus binário molda fortemente essa escolha na prática. Bibliotecas são esperadas para definir enums de erro precisos e frequentemente personalizados (frequentemente com thiserror para reduzir código repetitivo) porque os chamadores precisam distinguir programaticamente os modos de falha. Binários de aplicação mais frequentemente recorrem a um único tipo de erro dinâmico como anyhow::Error, porque no nível superior geralmente apenas se registra o erro ou se sai - a precisão extra de um enum personalizado tem retornos decrescentes quanto mais perto você chega de main.
A dependência do ? em conversões From é também o que impede que o código de tratamento de erros se torne uma parede de chamadas manuais .map_err(...) à medida que os erros cruzam limites de módulo ou crate. Um tipo de erro bem projetado implementa From para cada erro que ele precisa absorver, de modo que a propagação com ? permanece um único caractere em cada local de chamada falível, não importa quantas fontes de erro subjacentes uma função encadeie. É aqui que contexto de erro e backtraces e tipos de erro estruturados ganham seu valor em sistemas maiores: quanto mais longe um erro viaja de sua origem antes de ser registrado, mais esse contexto de origem precisa ser anexado deliberadamente, já que Rust não mantém uma pilha de chamadas implícita através da propagação de Result da maneira que uma exceção não tratada faz.
Código assíncrono muda pouco sobre este modelo conceitualmente - um Result retornado de um async fn é aguardado e combinado exatamente como um síncrono - mas significa que erros de tarefas concorrentes precisam de um caminho explícito de volta para o que quer que esteja coordenando-os, já que não há uma pilha de chamadas compartilhada para um desenrolamento estilo exceção viajar automaticamente.
Concepções Errôneas Comuns
- "
Resulté basicamente a versão Rust do try/catch."Resulté dado fluindo através de valores de retorno normais e correspondência de padrões, não um mecanismo de controle de fluxo separado que desenrola passando por código que nunca optou por participar - cada função intermediária tem que explicitamente propagá-lo ou tratá-lo. - "
?faz algo mágico além de um retorno antecipado." É açúcar sintático para "desempacotaOk, ou retornaErr(e.into())imediatamente" - a mesma coisa que você poderia escrever manualmente com ummatch, apenas mais curto. - "
panic!é o mecanismo de exceção do Rust, usado também para falhas esperadas." Panics são para bugs e invariantes quebrados, não para condições que um chamador deveria lidar rotineiramente - recorrer apanic!em uma falha esperada (um arquivo ausente, entrada de usuário inválida) é um sinal de design ruim, não tratamento de erro idiomático. - "
unwrap()é sempre má prática." É totalmente apropriado em testes, protótipos e casos onde a falha representa genuinamente um bug (um invariante que você já provou que é válido) - o problema é usá-lo em erros que um chamador poderia razoavelmente esperar e deveria tratar em vez disso. - "Você precisa de um tipo de erro personalizado para cada função que pode falhar." Binários simples e pequenas ferramentas geralmente se saem bem com
anyhow::ErrorouBox<dyn Error>; enums de erro personalizados valem o custo principalmente em bibliotecas onde os chamadores precisam combinar modos de falha específicos.
FAQs
Qual é a estrutura do `Result<T, E>`?
Um enum comum de duas variantes da biblioteca padrão: Ok(T) carregando um valor de sucesso, Err(E) carregando um valor de erro, combinado e correspondido exatamente como qualquer outro enum.
Como `Result` é diferente de uma exceção try/catch?
Uma exceção pode desenrolar silenciosamente por qualquer número de quadros de pilha que nunca declararam que poderiam lançar; um Result tem que ser explicitamente retornado e tratado (ou propagado com ?) por cada função na cadeia, e a possibilidade de falha é visível na assinatura de cada função.
Qual é a diferença real entre `Result` e `Option`?
Option<T> representa um valor que pode simplesmente estar ausente, sem necessidade de explicação (None); Result<T, E> representa uma operação que pode falhar com uma razão específica anexada (Err(E)), usado quando o "porquê" importa para o chamador.
O que o operador `?` realmente expande?
Aproximadamente: avalia a expressão Result, e se for Ok(v), produz v; se for Err(e), retorna Err(e.into()) da função envolvente imediatamente. É açúcar sintático para retorno antecipado, não um mecanismo de controle de fluxo distinto.
Por que tipos de erro personalizados frequentemente implementam `From` para outros tipos de erro?
Porque ? chama .into() no erro automaticamente durante a propagação, e implementar From<SourceError> for MyError é o que permite que um único ? converta um erro de I/O subjacente, erro de análise ou similar no próprio tipo de erro declarado da função sem um .map_err(...) manual em cada local de chamada.
Quando devo usar `panic!` em vez de `Result`?
Quando a falha representa um invariante quebrado ou um bug genuíno que o programa nunca deveria alcançar em operação correta, não uma condição que um chamador poderia razoavelmente esperar e querer tratar - um arquivo de configuração malformado é um Result; um índice que é matematicamente provado como dentro dos limites, mas de alguma forma não é, está mais próximo do território de panic!.
Chamar `.unwrap()` é sempre um erro?
Não - é apropriado em testes, protótipos rápidos e casos onde você já estabeleceu que o Result deve ser Ok (ou o Option deve ser Some) pela lógica circundante. Torna-se um problema quando usado em falhas que um chamador deveria ser capaz de observar e tratar em vez disso.
Rust tem algo como uma pilha de chamadas para erros baseados em `Result`?
Não automaticamente como uma exceção não tratada - um Result propagado através de ? carrega apenas o próprio valor de erro, a menos que você anexe contexto deliberadamente (através de .context() do anyhow, por exemplo) em cada camada pela qual ele passa.
Por que bibliotecas e binários tendem a tratar erros de forma diferente?
Bibliotecas geralmente expõem enums de erro personalizados precisos para que os chamadores possam combinar e responder a modos de falha específicos programaticamente; binários mais frequentemente usam um único tipo de erro dinâmico como anyhow::Error porque o nível superior geralmente apenas registra ou sai, independentemente de qual erro específico ocorreu.
O `?` pode converter automaticamente entre diferentes tipos de erro?
Sim, desde que o tipo de erro declarado da função implemente From<E> para qualquer tipo de erro E que a expressão falível produza - ? chama essa conversão implicitamente como parte de seu comportamento de retorno antecipado.
O que acontece quando ocorre um `panic` - o programa sempre trava imediatamente?
Por padrão, um panic desenrola a pilha da thread atual, executando destrutores ao longo do caminho, e então termina essa thread; uma configuração de perfil panic = "abort" pula o desenrolamento e termina o processo imediatamente em vez disso. De qualquer forma, ele não produz um valor que um chamador possa inspecionar e do qual possa se recuperar da maneira que um Result faz.
É apropriado capturar um `panic` e continuar a execução?
Raramente, e não deve ser tratado como uma estratégia geral de tratamento de erros - catch_unwind existe principalmente para isolar limites como um worker de pool de threads ou um sandbox de plugin de derrubar todo o processo, não como um substituto para usar Result em falhas que você espera.
Relacionados
- Noções Básicas de Tratamento de Erros - o guia prático que esta página ancora
- O Operador ? - mecânicas de propagação em profundidade
- panic! vs Erros Recuperáveis - traçando a linha na prática
- Tipos de Erro Personalizados - projetando enums de erro com conversões
From - Modelando Dados com Structs e Enums - os fundamentos de tipo soma em que
ResulteOptionsão construídos
Versões da Pilha: Esta página foi escrita para Rust 1.97.0 (edição 2024), Tokio 1.x, Axum 0.8, serde 1.0, sqlx 0.8, clap 4, e Polars 0.46+.