Tratamento de erros
monitor, handle tipado, ensure, guard, validate, retry, propagate, defer e stack traces.
monitor / handle / ensure#
monitor:
x := 10 / 0
handle e:
out $"capturado: {e.type} — {e.message}"
ensure:
out "sempre roda"capturado: RuntimeError — Division by zero
sempre rodaO objeto de erro#
O nome ligado pelo handle expõe:
| Campo | Contém |
|---|---|
.type | o nome do tipo, ex. "RuntimeError" |
.message | a mensagem |
.line .column | a posição de origem |
Ele também se comporta como texto ao ser concatenado ou comparado com uma String.
handle tipado#
monitor:
x := 1 / 0
handle RuntimeError as e:
out $"só pego erro de execução: {e.message}"Se o erro não casar com o tipo, ele continua subindo — não é capturado ali. Os tipos disponíveis estão em Hierarquia de erros.
Vários handle#
Um monitor aceita uma cláusula por tipo de erro. Vence a primeira que casar — a ordem importa, como nos point de um match:
action ler(vault, chave, divisor):
monitor:
yield vault[chave] / divisor
handle KeyError:
yield 0
handle DivisionByZeroError as e:
out e.message
yield 0
handle Error as e:
propagateSem isso, tratar dois erros de formas diferentes obrigava a capturar Error e despachar na mão com um match e.type — que é exatamente o que o handle tipado existe para evitar.
Lançar#
| Forma | Efeito |
|---|---|
trigger <expr> | lança TriggerError com a mensagem |
guard <cond>, <msg> | lança se a condição for falsa |
guard <cond> otherwise: bloco | roda o bloco e sai da ação |
validate <expr>, <msg> | lança se o valor for falso |
propagate <expr> | relança, depois de registrar |
assert <cond>, <msg> | lança RuntimeError_ se falso |
action sacar(conta, valor):
guard valor bigger 0, "valor precisa ser positivo"
guard valor smaller_eq conta.saldo, "saldo insuficiente"
yield conta with {"saldo": conta.saldo - valor}guard na entrada da ação documenta as pré-condições em duas linhas, sem aninhar o corpo inteiro num given.
retry#
Repete um bloco até conseguir. Se todas as tentativas falharem, o handle roda com o último erro:
tentativas := {"n": 0}
retry 5:
tentativas["n"] := tentativas["n"] + 1
given tentativas["n"] smaller 3:
trigger "instabilidade temporaria"
out $"sucesso na tentativa {tentativas["n"]}"
handle e:
out $"desistiu: {e}"propagate — registrar e repassar#
action camada_media():
monitor:
camada_baixa()
handle e:
registro.error("falha na camada baixa", {"motivo": e.message})
propagate e.messageA camada do meio anota o que sabe — contexto que o topo não teria — mas não decide o que fazer. Essa decisão pertence a quem tem visão do todo.
O anti-padrão oposto é engolir: handle e: registro.error("falhou") faz o chamador achar que deu certo.
defer — limpeza garantida#
action processar():
IO.write(temp, "dados")
defer:
IO.delete(temp) # roda em QUALQUER caminho de saída
given deve_falhar:
trigger "erro no meio"
yield IO.read(temp)| Saída | defer roda? |
|---|---|
| chegou ao fim | sim |
yield no meio | sim |
trigger / erro | sim |
| erro vindo de uma ação chamada | sim |
Vários defer rodam em ordem inversa (LIFO) — a ordem correta para desmontar recursos dependentes.
defer ou ensure?#
defer | ensure | |
|---|---|---|
| Escopo | a ação inteira | um bloco monitor |
| Declaração | junto da aquisição | no fim do bloco |
| Vários | sim, em LIFO | um por monitor |
Stack traces#
Um erro não tratado traz o arquivo, a linha, o trecho do código e a cadeia de chamadas:
RuntimeError: Division by zero
em calculadora.df:12:15
12 | yield total / divisor
| ^
Pilha de chamadas (mais recente primeiro):
em media calculadora.df:12
em relatorio calculadora.df:28
em main calculadora.df:45Onde tratar#
Trate o erro onde você pode fazer algo a respeito. Nas camadas intermediárias, registre e repasse. No topo, decida: mostrar ao usuário, tentar de novo, ou abortar.