Diagnosing Rust Compiler and Runtime Errors
Rust's error messages have a reputation for being unusually long, and that reputation obscures a more useful fact: they are long because they are trying to teach you something specific. Every borrow-checker or lifetime error encodes a reasoning process the compiler just performed, and that process is learnable in the same way a math proof's steps are learnable. This page is not about any single error code; it is about the skill of reading diagnostics and about a triage model for deciding which toolchain a given failure needs, since a borrow-checker error, a panic, and an async deadlock are three genuinely different problems wearing similar-looking stack traces.
Resumo
- Falhas do Rust se dividem em três classes distintas (erros de tempo de compilação de propriedade/trait, pânicos em tempo de execução e travamentos de concorrência em tempo de execução), e cada classe tem sua própria anatomia de diagnóstico e sua própria toolchain.
- Por que Importa: Tratar todos os erros do Rust como "o compilador sendo difícil" desperdiça tempo; reconhecer a classe primeiro diz se você deve ler um diagnóstico, capturar um backtrace ou anexar uma ferramenta de rastreamento.
- Conceitos-Chave: anatomia de diagnóstico (span primário, span secundário, ajuda/nota), região de empréstimo (borrow region), classe de falha, backtrace, travamento do executor (executor stall), violação de limite de trait (trait bound violation).
- Quando Usar: Diante de um
error[E0xxx]desconhecido, um pânico em produção, um serviço que trava sem falhar ou uma rejeição de limiteSend/Syncao criar uma tarefa. - Limitações / Trade-offs: Este modelo acelera o triage, mas não substitui o conhecimento de domínio; alguns erros abrangem mais de uma classe, e desvendar esses ainda leva prática.
- Tópicos Relacionados: borrow checker, lifetimes, panics, Send e Sync, runtimes assíncronos, observabilidade.
Fundamentos
Um diagnóstico do compilador Rust é estruturado como um argumento curto, não como um despejo de estado interno.
Ele começa com um código de erro e uma afirmação de uma linha, como "não é possível emprestar x como mutável porque ele também está emprestado como imutável".
Abaixo disso, um span primário aponta para o token exato onde o conflito se tornou visível, e um span secundário (frequentemente rotulado com uma nota) aponta para o empréstimo anterior que causou o conflito.
Finalmente, a maioria dos diagnósticos termina com uma sugestão help: que propõe uma correção textual concreta, e essa sugestão é gerada a partir da mesma análise que produziu o erro, não de um modelo genérico.
Ler um diagnóstico na ordem (afirmação, evidência, sugestão de correção) transforma um muro de texto vermelho em um argumento de três etapas que você pode verificar ou rejeitar.
A segunda ideia fundamental é que as falhas do Rust não são uma categoria indiferenciada chamada "erros".
Um erro de tempo de compilação significa que o programa nunca produziu um binário, então nada foi executado ainda e a correção vive inteiramente no código-fonte.
Um pânico em tempo de execução significa que um binário foi executado e depois desenrolado porque um invariante que ele assumia (um Option sendo Some, um índice dentro dos limites) se mostrou falso.
Um travamento de concorrência significa que o binário ainda está em execução, não entrou em pânico e não está produzindo progresso, geralmente porque uma tarefa assíncrona está bloqueada em algo que nunca será concluído.
Essas três classes parecem semelhantes para um novato (todas elas impedem o programa de fazer seu trabalho), mas exigem evidências diferentes: raciocínio em nível de código-fonte para a primeira, um backtrace para a segunda e uma visão ao vivo do estado da tarefa para a terceira.
Mecânicas e Interações
O passe de verificação de empréstimos (borrow-checking) do compilador funciona rastreando, para cada referência, a região (span de código) durante a qual essa referência é considerada ativa, e rejeita qualquer programa onde duas regiões incompatíveis se sobreponham. Lifetimes não léxicos encolhem uma região para o último uso real de uma referência em vez do final de seu bloco envolvente, é por isso que um empréstimo às vezes pode "terminar" antes do que o escopo da variável sugere visualmente. Quando o verificador relata um conflito, ele está relatando uma sobreposição entre duas regiões que ele computou, e o span secundário no erro está mostrando literalmente onde o compilador decidiu que a primeira região começou. É por isso que "encurtar um empréstimo" tão frequentemente corrige o erro: você não está apaziguando o compilador, você está encolhendo a região para que a sobreposição que ele detectou não exista mais.
Erros de Send e Sync funcionam através de um mecanismo completamente diferente: resolução de traits, não análise de regiões.
Quando você tokio::spawn um future, o runtime exige que o tipo desse future implemente Send, e o compilador deriva Send automaticamente para um tipo apenas se cada campo que ele fecha (captura) também for Send.
Um erro aqui é realmente o compilador percorrendo seu estado capturado e relatando o primeiro campo não-Send que encontrou, comumente um Rc<T> ou um RefCell guard mantido em um ponto de .await.
Como esta é uma resolução de trait em vez de rastreamento de empréstimos, nenhuma reestruturação do fluxo de controle ajuda; a correção é sempre mudar o tipo que está sendo capturado, geralmente trocando Rc/RefCell por Arc/um mutex assíncrono.
Pânicos em tempo de execução e travamentos de concorrência divergem tão acentuadamente um do outro, e misturar suas toolchains é o erro de triage mais comum.
Um pânico desenrola a pilha e, com RUST_BACKTRACE=1, imprime exatamente a cadeia de chamadas que levou ao unwrap, expect, índice ou estouro aritmético falho, então a evidência que você precisa já existe nesse único backtrace.
Um travamento não produz nenhum backtrace, porque nada entrou em pânico; o programa está simplesmente esperando em um future, um lock ou um canal que nunca resolverá, então você precisa de uma ferramenta que possa inspecionar o estado da tarefa ao vivo, como spans de tracing ou tokio-console, em vez de uma ferramenta que inspeciona um crash.
// Uma rejeição com limite Send é um erro de trait, não um erro de borrow - a correção
// muda *qual tipo* é capturado, não por quanto tempo algo é emprestado.
use std::rc::Rc;
use std::sync::Arc;
fn spawn_shareable(counter: Arc<std::sync::atomic::AtomicU64>) {
// Rc<T> aqui falharia ao compilar dentro de tokio::spawn: não é Send.
// Arc<T> compila porque o limite Send do Arc só precisa de T: Send + Sync.
tokio::spawn(async move {
counter.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
});
}Considerações Avançadas e Aplicações
Aplicar este modelo de triage em escala significa escolher o instrumento certo antes de começar a ler qualquer coisa.
Para erros de tempo de compilação, rustc --explain E0502 (ou o código que rust-analyzer já mostra inline) fornece a explicação canônica para esse código de erro, e vale a pena lê-lo uma vez por código na primeira vez que você o encontrar, em vez de redescobrir a regra a cada vez.
Para pânicos, capturar um backtrace completo em produção (RUST_BACKTRACE=full atrás de um feature flag, ou um panic hook que reporta ao seu rastreador de erros) transforma um relatório de crash opaco em um argumento de três etapas semelhante ao que um diagnóstico do compilador fornece: onde falhou, o que chamou o quê e qual suposição estava incorreta.
Para travamentos de concorrência, tokio-console fornece uma tabela ao vivo de tarefas com suas contagens de poll e estados de espera, que é a única maneira de distinguir "a tarefa está fazendo trabalho real lentamente" de "a tarefa está parada em um lock há dez minutos".
Para problemas de segurança de memória que se escondem dentro de blocos unsafe ou limites FFI, nenhum dos itens acima ajuda diretamente; essa é uma quarta classe, mais restrita, tratada por ferramentas de sanitização como Miri ou um fuzzer, e merece seu próprio caminho de diagnóstico separado em vez de ser agrupada com pânicos.
| Classe de Falha | Sinal | Ferramenta Certa | Causa Raiz Comum |
|---|---|---|---|
| Tempo de Compilação (borrow/lifetime/trait) | error[E0xxx] antes que qualquer binário rode | Ler spans de diagnóstico; rustc --explain | Regiões de empréstimo sobrepostas ou uma captura não-Send |
| Pânico em Tempo de Execução | Processo sai, backtrace disponível | RUST_BACKTRACE=1, panic hook, testes | unwrap/expect não verificado, índice fora dos limites, estouro |
| Travamento de Concorrência | Processo vivo, sem progresso, sem pânico | Spans de tracing, tokio-console | Chamada bloqueante em um thread do executor, lock mantido em um .await, ordem de lock incompatível |
A habilidade mais profunda que esta tabela sugere é a depuração orientada por diagnóstico: antes de escrever qualquer código de reprodução, decida em qual linha você está, pois a linha determina se você deve estar lendo código-fonte, lendo uma pilha ou observando um painel ao vivo.
Equipes que pulam esta etapa tendem a usar depuração com println! independentemente da classe de falha, o que funciona para pânicos, desperdiça tempo em erros de borrow (a correção quase nunca é visível a partir de declarações print adicionadas) e induz a erro ativamente em travamentos (adicionar logging pode mudar o agendamento o suficiente para mascarar o próprio travamento que você está perseguindo).
Conceitos Equivocados Comuns
- "O borrow checker é arbitrário e eu só preciso satisfazê-lo." - Ele está aplicando uma regra específica e verificável sobre regiões sobrepostas; a regra é aprendível e o mesmo punhado de padrões (realocação invalidando uma referência, mantendo um empréstimo durante uma mutação) respondem pela maioria dos erros.
- "Uma mensagem de erro longa significa um bug complicado." - O comprimento da mensagem se correlaciona com o contexto que o compilador achou necessário para justificar sua afirmação, não com a dificuldade da correção; muitos dos diagnósticos mais longos têm correções de uma linha.
- "Erros de Send/Sync são sobre threads que escrevi errado." - Eles são quase sempre sobre um tipo que você capturou, não sobre a lógica que você escreveu; a correção é mudar um tipo (
RcparaArc,RefCellpara um mutex) em vez de reestruturar o fluxo de controle. - "Se não está entrando em pânico, não está quebrado." - Um programa assíncrono travado ou bloqueado não produz nenhum erro, é por isso que o silêncio é em si um sinal de diagnóstico que pertence a uma toolchain diferente da dos pânicos.
- "Mais backtraces tornam travamentos assíncronos mais fáceis de depurar." - Um backtrace só existe após um unwind; um travamento nunca desenrola, então backtraces são o instrumento errado e usá-los desperdiça a etapa de triage.
FAQs
O que realmente significa "ler" um erro do compilador Rust?
- Leia primeiro a afirmação de uma linha (o código e a mensagem de erro).
- Encontre o span primário (onde o conflito surgiu) e o span secundário ou nota (onde o estado anterior foi configurado).
- Leia a linha
help:por último, pois é uma sugestão gerada a partir da mesma análise, não uma segunda opinião independente.
Por que erros do borrow-checker são frequentemente corrigidos encurtando o escopo de uma variável?
Lifetimes não léxicos significam que a região ativa de uma referência termina em seu último uso real, não no final de seu bloco. Encurtar o código entre um empréstimo e a operação conflitante encurta diretamente essa região até que as duas operações não se sobreponham mais no tempo.
Como o compilador decide que um tipo não implementa Send?
Send é derivado automaticamente: uma struct ou closure é Send apenas se cada campo ou variável capturada que ela contém for Send.
O compilador percorre o estado capturado e relata o primeiro campo não-Send que encontra, é por isso que a correção visa um tipo específico (comumente Rc ou um ponteiro bruto) em vez da lógica circundante.
Como um deadlock assíncrono é realmente diferente de um pânico internamente?
Um pânico desenrola a pilha, é por isso que ele produz um backtrace que você pode ler depois do fato. Um travamento nunca desenrola; a tarefa ainda está tecnicamente "em execução" do ponto de vista do runtime, apenas parada em algo que nunca resolve, então não há artefato de crash para inspecionar e você precisa de uma ferramenta de estado ao vivo em vez disso.
Devo sempre ativar RUST_BACKTRACE=full em produção?
Backtraces completos adicionam sobrecarga a cada caminho de pânico e podem vazar caminhos de arquivo internos para logs, então a maioria das equipes o limita por um flag ou o ativa apenas em staging.
Um RUST_BACKTRACE=1 simples com um panic hook estruturado que reporta ao seu rastreador de erros cobre a maioria das necessidades de triage com menor custo.
Quando não vale a pena perseguir um erro Send/Sync manualmente?
Se uma struct grande falha em ser Send por causa de um campo profundamente aninhado, rastrear esse campo manualmente pode levar mais tempo do que reestruturar o tipo para conter estado compartilhável (Arc, canais) na fronteira.
Nesse caso, redesenhar a fronteira de estado compartilhado geralmente é mais rápido do que iterar sobre a saída do compilador campo por campo.
Um travamento é apenas um pânico muito lento?
Não: um travamento não tem nenhuma asserção falha e nenhum unwind, então nenhuma das toolchains de pânico (backtraces, catch_unwind, panic hooks) o observa.
Diagnosticar um travamento requer instrumentar o programa ao vivo (spans de tracing, tokio-console, ou timeouts manuais) em vez de esperar por um artefato de crash que nunca chegará.
Por que dois desenvolvedores lendo o mesmo erro de borrow às vezes propõem correções diferentes?
A correção sugerida pelo compilador visa o conflito imediato, mas a sobreposição de região que ele detectou é frequentemente um sintoma de uma escolha de design de propriedade mais ampla. Um desenvolvedor pode corrigir o escopo local enquanto outro reestrutura a propriedade inteiramente; ambos podem compilar, mas eles trocam de forma diferente em termos de manutenibilidade futura.
O rust-analyzer muda a forma como devo ler os erros?
Ele exibe os mesmos spans e códigos inline enquanto você digita, o que encurta o ciclo de feedback, mas não muda o processo de raciocínio subjacente; a ordem de leitura de três etapas (afirmação, evidência, sugestão) ainda se aplica.
Qual é a maneira mais rápida de saber a qual classe de falha estou olhando?
Pergunte em ordem: ele falhou ao compilar (classe de tempo de compilação), o processo saiu com um backtrace (classe de pânico), ou o processo está vivo, mas não progredindo (classe de travamento). Essa única pergunta o direciona para a ferramenta certa antes que você escreva qualquer código de diagnóstico.
Miri e sanitizers fazem parte deste modelo de triage?
Eles cobrem uma quarta classe mais restrita: comportamento indefinido dentro de código unsafe que a toolchain de Rust seguro acima não consegue ver.
Se um bug só se reproduz com unsafe, FFI ou ponteiros brutos envolvidos, use Miri ou um sanitizer em vez de tentar forçá-lo nas categorias de pânico ou travamento.
Por que adicionar declarações `print` às vezes faz um travamento assíncrono desaparecer?
Chamadas de logging e print mudam o agendamento e o timing das tarefas, e um travamento causado por uma race condition estreita ou um problema de ordem de lock pode mudar ou desaparecer sob diferentes timings.
Este é um forte sinal de que você está na classe de travamento, não na classe de pânico, já que pânicos são determinísticos dados a mesma entrada e declarações print raramente mudam se elas serão disparadas.
Quanta dessa habilidade de triage é transferível para outras linguagens?
O instinto geral (separar erros de tempo de compilação, crashes e hangs em caminhos de diagnóstico diferentes) é transferível amplamente, mas os mecanismos específicos (regiões de empréstimo, auto-derivação de Send) são específicos do Rust e não se mapeiam diretamente para linguagens com coleta de lixo.
Relacionados
- Borrow-Checker Error Scenarios - a classe de tempo de compilação em profundidade, com códigos E comuns e padrões de correção.
- Panic & Unwrap Scenarios - a classe de pânico em tempo de execução, incluindo quando manter vs. remover
unwrap. - Send/Sync & Thread Errors - a classe de limite de trait que a seção Mecânicas desta página percorre.
- Async Deadlocks & Blocking - a classe de travamento de concorrência e as ferramentas que observam o estado da tarefa ao vivo.
- Debugging Tools - a lista de ferramentas concretas (
dbg!, tracing, debuggers) referenciadas em todas as três classes de falha. - The Borrow Checker - o modelo de raciocínio baseado em região que sustenta a classe de tempo de compilação.
Versões de Stack: 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+.