Nos últimos anos, tornou-se quase um clichê dizer que “exceções são coisa do passado” e que results e erros tipados são o futuro.

Contudo, muitas dessas discussões perdem o ponto principal: não se trata apenas de sintaxe ou de modismo. A questão é como as linguagens de programação modelam falhas — se como um evento de controle de fluxo ou como parte do contrato de tipos de uma função.

Nota: Aqui estou falando apenas de erros esperados e recuperáveis, como validações, falhas de I/O ou operações que podem falhar legitimamente. Erros imprevisíveis, que normalmente interrompem o programa — como panic! em Rust ou RuntimeException em Java — não são considerados neste artigo. Esses pertencem à categoria de falhas irrecuperáveis, e seguem outra filosofia de tratamento.

Duas filosofias opostas: Go e Java

Para entender onde tudo começou, vale comparar duas escolas de pensamento totalmente opostas.

Go — erros como valores simples

Go adota uma abordagem propositalmente simples: sem exceções, sem stack unwinding, sem mágica do compilador.

Toda função que pode falhar retorna um segundo valor — um erro — e cabe a você decidir o que fazer com ele.

 1func saveUser(u User) error {
 2    if err := validate(u); err != nil {
 3        return err
 4    }
 5    if err := persist(u); err != nil {
 6        return err
 7    }
 8    return nil
 9}
10
11func main() {
12    if err := saveUser(user); err != nil {
13        log.Println(err)
14    }
15}

Vantagens

  • Previsível e leve: um erro é apenas outro valor de retorno.
  • Nenhum controle de fluxo oculto — tudo é explícito.
  • Fácil de entender, especialmente para quem está começando.

Desvantagens

  • Verbosidade: o if err != nil repetido adiciona ruído.
  • Difícil de compor: o tratamento de erro não encadeia naturalmente.
  • Tipagem limitada: error é uma interface, não um tipo genérico — o que reduz a precisão.
  • Um leve, mas, ainda existente, custo de runtime: cada erro é um valor de interface que armazena metadados de tipo e um ponteiro, o que adiciona alguma indireção. Não é dramático, mas não é gratuito.

O principal trade-off no modelo do Go é a simplicidade em detrimento da abstração. Você sempre sabe exatamente o que acontece, mas paga por isso em verbosidade e falta de capacidade de composição.

Java — checked exceptions e tratamento obrigatório

O design do Java apostou no caminho oposto: falhas devem fazer parte do contrato da função.

O compilador força você a lidar com as exceções ou declará-las com throws.

 1public void saveUser(User user) throws ValidationException, SQLException {
 2    validate(user);
 3    persist(user);
 4}
 5
 6public static void main(String[] args) {
 7    try {
 8        saveUser(new User());
 9    } catch (ValidationException | SQLException e) {
10        log.error(e.getMessage());
11    }
12}

Vantagens

  • O compilador impõe disciplina: você não pode ignorar uma falha declarada.
  • As assinaturas dos métodos mostram explicitamente quais erros podem ocorrer.

Desvantagens

  • Verbosidade e intrusão: try/catch se espalha pelas camadas do código.
  • Custo de performance: lançar exceções dispara o stack unwinding. Isso significa que o runtime precisa percorrer a pilha de chamadas até encontrar um bloco catch compatível, executando códigos de limpeza (finally) pelo caminho. Esse processo envolve capturar stack traces e invalida otimizações — é relativamente caro.
  • Semântica dividida: o tipo de retorno (void, User, etc.) não representa a falha — é um canal de controle separado.

Versões mais recentes do Java (17+) começaram a experimentar com pattern matching para exceções, e projetos como o Project Amber estão introduzindo um tratamento mais orientado a expressões.

No entanto, esses aprimoramentos são recursos opt-in — tornam as exceções menos dolorosas, mas não mudam o modelo central. Exceções em Java ainda são controle de fluxo em tempo de execução, fora do sistema de tipos.

Rust: um meio-termo que o tornou prático

Rust fica entre esses dois mundos.

Como o Go, rejeita exceções como mecanismo de controle.
Como o Java, torna o tratamento de falhas explícito e verificado pelo compilador.

Tudo gira em torno do tipo Result<T, E>:

1fn save_user(user: &User) -> Result<(), AppError> {
2    validate(user)?;
3    persist(user)?;
4    Ok(())
5}

O operador ? é uma abreviação para:

1match validate(user) {
2    Ok(_) => (),
3    Err(e) => return Err(e.into()),
4}

Isso é funcionalmente equivalente ao if err != nil { return err } do Go, mas com tipagem forte e zero custo de runtime — o compilador expande para um simples retorno condicional.

Não há stack unwinding, nem alocação no heap, nem dynamic dispatch. Erros são apenas dados, representados como enums totalmente conhecidos pelo compilador.

O que Rust herda de cada modelo

Aspecto Java Go Rust
Erros fazem parte do contrato da função sim (throws) sim (error como retorno) sim (Result<T, E>)
Stack unwinding no fluxo normal sim não não
Tratamento obrigatório pelo compilador sim não (disciplina apenas) sim
Custo de runtime alto baixo (indireção por interface) desprezível (despacho em tempo de compilação)
Controle de fluxo explícito não (lançamento implícito) sim sim
Verbosidade alta alta baixa

Conceitualmente, Rust está mais próximo de Java em garantias de segurança — você não pode ignorar um erro acidentalmente — mas mais próximo de Go em semântica, já que um erro ainda é um valor de retorno, não um salto de execução.

Quando o modelo de Rust não é perfeito

A abordagem de Rust tem seus próprios trade-offs.

Como toda falha possível precisa aparecer no sistema de tipos, funções podem rapidamente se encher de Result e Option aninhados. A composição genérica de erros (From/Into ou thiserror) reduz o ruído, mas adiciona uma camada de abstração que nem sempre compensa em programas pequenos.

Rust também não tem a flexibilidade de “sair de qualquer lugar” que as exceções permitem — às vezes, um throw é realmente a maneira mais limpa de sair de múltiplas camadas de lógica. A simplicidade do Go também pode parecer mais natural em scripts, ferramentas ou APIs onde os erros são apenas logados e ignorados localmente.

Portanto, embora Rust ofereça segurança e capacidade de composição máximas, ele também exige que o desenvolvedor modele cada falha explicitamente — uma vantagem em sistemas grandes, mas talvez exagero em programas pequenos.

Semântica e visibilidade

A diferença central não está em se a linguagem obriga o tratamento, mas em quanto o compilador sabe sobre o erro.

  • Em Java, tanto User save() quanto User save() throws SQLException retornam o mesmo tipo (User); o canal de erro existe fora do sistema de tipos.
  • Em Rust, fn save() -> Result<User, DbError> é um tipo distinto — o compilador rastreia cada propagação ao longo da cadeia de chamadas.

É por isso que a abordagem de Rust favorece a composição: você pode mapear, transformar ou mesclar erros como valores de primeira classe, sem que a linguagem precise “saltar” para fora da função.

O panorama geral

Nenhum desses modelos é universalmente “melhor”. Cada um otimiza para um equilíbrio diferente entre segurança, simplicidade e expressividade.

Linguagem Filosofia O que otimiza
Java Falhas como controle de fluxo Segurança por imposição, ao custo de performance e verbosidade
Go Falhas como valores Simplicidade e previsibilidade, ao custo de repetição
Rust Falhas como valores tipados Segurança em tempo de compilação e componibilidade, ao custo de complexidade e verbosidade em assinaturas de tipos grandes

Com o tempo, as linguagens de programação vêm convergindo para um mesmo princípio: tornar as falhas explícitas, previsíveis e conhecidas pelo tipo.

Rust não inventou essa ideia — apenas deu a ela uma forma prática e ergonômica que se encaixa na programação de sistemas moderna.