Pular para o conteúdo

Racional · ensaio 05

Erros

rationale.md · 80 linhas · 3 min de leitura

A redefinição: erro é valor, não evento de controle. Result[T,E] + error-sets, propagação explícita (sem ?), e nada de unit.

Há duas grandes escolas para falha. Exceções (Java/Python/C++): o erro é um evento que “explode” e sobe a stack até alguém pegar, invisível no tipo da função. Erro-como-valor (Go/Rust): a falha é um valor de retorno que você inspeciona. Makoto fica firmemente na segunda escola, e radicaliza a explicitude.

Falha tem duas perguntas, e há um construto para cada:

  • Result[T, E] é a resolução: Ok(T) ou Err(E). É uma família de aridade 2, igual a Map[K,V], então compõe com generics (map sobre o Ok, collect de List[Result[T]] para Result[List[T]]). Quem recebe um Result não pode ignorar a falha: ela está no tipo.
  • error{...} é a união: quais falhas existem. Fechado (error{A, B}, exaustivo, proíbe _), aberto (error pelado, exige _), ou vazio (error{} = “não erra”, Err inconstruível).

Propagação é explícita, por trailer, nunca por operador: v := do() catch |e| { return e } propaga; catch |e| trata o erro como unidade; match |e| ramifica por variante numa tacada (vincula e discrimina, sem o duplo-binding “primeiro catch, depois match”). E sucesso-sem-valor não usa unit: é o error-set sozinho (fn send(m) -> error{...}), onde sucesso = ausência de erro. noreturn é o tipo bottom da divergência (panic/exit), que faz um braço de erro “escapar” sem dar valor.

A inspiração declarada é Go (forçar a árvore de recuperação a ser desenhada linha a linha), mas com uma torção: em vez de repetir if err != nil, você escreve catch/match |e| uma vez, e o compilador exige que você trate ou propague de propósito. O ganho central é matar o hidden flow: você nunca propaga um erro por acidente; cada passo é uma decisão visível (“trato aqui ou repasso?”).

A escolha contra unit é economia conceitual pura: não multiplicar tipos vazios. Se o sucesso não carrega valor, o error-set sozinho já diz tudo; se você precisa de sucesso como valor de composição, Result[(...), E] resolve, mas o caso comum (procedimento que pode falhar) não paga por um tipo fantasma.

  • Exceções. Fluxo de controle escondido: você chama fetch_user(id) e ela pode ejetar NetworkError/ParseError/Timeout para qualquer nível da stack, sem nada no tipo. O contrato de falha fica invisível. Erro-como-valor o torna parte da assinatura.
  • O operador ? (Rust/Zig). É o mesmo perigo do coloring incontível, aplicado a erro: vira a forma de fugir de tratar a falha, um glifo que propaga sozinho. A linguagem deve fazer o custo da propagação visível; catch |e| { return e } é mais longo de propósito, e é essa fricção que mata a negligência. (Note o paralelo com unsafe: a diferença é que unsafe é contível e ? só esconde.)
  • unit/void como tipo. Tipo vazio que não carrega informação nenhuma; o error-set pelado ocupa o lugar sem o fantasma.
  • -> (T, error) ao estilo Go cru. Permite o nil, nil ambíguo e não compõe com generics. Makoto empurra para a forma canônica: -> (u8, error) gera um aviso “use Result[u8, error]”.

A função em Java cujo throws mente (ou não existe), e o NullPointerException que sobe de seis camadas abaixo. O unwrap()/? em Rust que vira hábito e engole falhas em código que deveria ser robusto. O if err != nil repetido de Go que é chato mas (reconhece-se) funciona, porque força você a olhar cada falha. Makoto quer o efeito de Go (atenção obrigatória a cada falha) sem a repetição e sem a porta de fuga do ?.

Erro é um valor que você trata ou propaga, explicitamente. Falha é uma saída do programa, não um evento que o runtime resolve por você.

Verbosidade é o preço, e é deliberado: você não encadeia do_a()?.do_b()?.do_c()?; monta a árvore de recuperação passo a passo. O spread de error-sets (error{open..., parse...}) adiciona um pouco de maquinaria, mas é o que evita reenumerar a lista inteira de variantes de cada subsistema. A aposta: cada linha que propaga custar a ser escrita é o que garante que nenhuma falha é esquecida.

Próximo: 06 · Ausência