Pular para o conteúdo

Tratamento de erros

monitor, handle tipado, ensure, guard, validate, retry, propagate, defer e stack traces.

monitor / handle / ensure#

dataforge
monitor:
    x := 10 / 0
handle e:
    out $"capturado: {e.type}{e.message}"
ensure:
    out "sempre roda"
saída
capturado: RuntimeError — Division by zero
sempre roda

O objeto de erro#

O nome ligado pelo handle expõe:

CampoContém
.typeo nome do tipo, ex. "RuntimeError"
.messagea mensagem
.line .columna posição de origem

Ele também se comporta como texto ao ser concatenado ou comparado com uma String.

handle tipado#

dataforge
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:

dataforge
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:
        propagate

Sem 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#

FormaEfeito
trigger <expr>lança TriggerError com a mensagem
guard <cond>, <msg>lança se a condição for falsa
guard <cond> otherwise: blocoroda 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
dataforge
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:

dataforge
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#

dataforge
action camada_media():
    monitor:
        camada_baixa()
    handle e:
        registro.error("falha na camada baixa", {"motivo": e.message})
        propagate e.message

A 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#

dataforge
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ídadefer roda?
chegou ao fimsim
yield no meiosim
trigger / errosim
erro vindo de uma ação chamadasim

Vários defer rodam em ordem inversa (LIFO) — a ordem correta para desmontar recursos dependentes.

defer ou ensure?#

deferensure
Escopoa ação inteiraum bloco monitor
Declaraçãojunto da aquisiçãono fim do bloco
Váriossim, em LIFOum por monitor

Stack traces#

Um erro não tratado traz o arquivo, a linha, o trecho do código e a cadeia de chamadas:

text
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:45

Onde 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.