Error Handling

code specs/code/error-handling.kmd

Padrões cross-language pra tratamento de erro internal: errors-as-values vs exceptions per-lang, error wrapping/chaining, panic/abort policy, recovery boundaries (top-level, request, goroutine), regra "log XOR throw" (não fazer ambos), sentinel/typed errors, resource cleanup on error. Distinto de errors/user-facing-messages.kmd que cobre só texto exibido ao usuário.

When this spec applies

Primary triggers

All triggers

Specification body

Spec — Error Handling

Facet Code do Koder Design.

Cobre erros internal (entre componentes/funções). User-facing error text é spec separada (errors/user-facing-messages.kmd). Auto-report de erros é errors/reporting.kmd.

Estilo per-language

A Koder respeita o idiom da linguagem — não força exceptions em Go nem Result<T, E> em Python.

Linguagem Estilo canônico Nota
Go error return value (last) Idiom puro Go; panic só em init/programmer error
Rust Result<T, E> + ? Panic só em invariant violation, não em recoverable error
Koda Exceptions (raise, rescue) Ruby-style; usar specific class, não Exception genérico
Dart Exceptions (throw, try/catch) Use specific Exception subclass
Python Exceptions (raise, try/except) Subclass de Exception; nunca catch BaseException
JS/TS Exceptions (throw, try/catch) TS: prefer typed Error subclass + discriminated union pra known cases
Shell Exit codes 0 = success; convention 1-127 specific; usar set -euo pipefail
SQL Specific (NOT NULL, CHECK, RAISE) Database-level constraints + transaction rollback

R1 — Error wrapping / chaining

Erro propagado entre layers deve ser wrapped com contexto:

Go

if err != nil {
    return fmt.Errorf("loading user %s: %w", userID, err)
}

(%w preserva unwrappable chain; errors.Is / errors.As na borda.)

Rust

use anyhow::{Context, Result};

let config = load_config(path)
    .with_context(|| format!("loading config from {}", path.display()))?;

Python

try:
    user = load_user(user_id)
except DatabaseError as e:
    raise UserLoadError(f"loading user {user_id}") from e

(from e preserva traceback.)

Koda

begin
  user = load_user(user_id)
rescue DatabaseError => e
  raise UserLoadError, "loading user #{user_id}: #{e.message}", cause: e
end

Princípio

  • Cada layer adiciona contexto (qual operação, qual ID/recurso); não duplica a mensagem da camada inferior
  • Original error preservado (chain) pra debugging; nunca "swallow"

R2 — Panic / abort policy

Quando OK Quando proibido
init() falhou (config inválida na boot) Erro recuperável vindo de input do usuário
Invariant violation interna (programmer error) Erro de IO, network, parsing externo
assert!() em debug builds Hot path em production runtime
Stack overflow / OOM (system-level) Default catch-all

Regra: panic = "estado impossível atingido; programa não pode continuar com segurança". Tudo que é "isso pode acontecer no mundo real" é erro recuperável, não panic.

R3 — Recovery boundaries

Top-level try/catch só em boundaries definidos:

Boundary Linguagem Padrão
main / init Todas Catch-all + log estruturado + exit code
HTTP/RPC handler Server Middleware envolve handler; converte pra response code apropriado
Goroutine boundary Go defer recover() no entry da goroutine; report panic via channel pra main
Worker queue task Todas Catch-all + retry policy + dead-letter
FFI / cgo callback Rust/Go Wrap em catch_unwind antes de cruzar boundary

Não envolver toda função em try/catch defensivo; exception propaga até o boundary apropriado.

R4 — Log XOR throw (regra central)

Quando erro acontece, escolher uma:

# ❌ Faz ambos — log no caller também → log spam
try:
    foo()
except Exception as e:
    logger.exception("foo failed")  # já loga
    raise                            # caller também vai logar
# ✅ Throw — caller decide log/handle
try:
    foo()
except Exception as e:
    raise FooError(...) from e

# OU

# ✅ Log + handle — não propaga (boundary final)
try:
    foo()
except Exception as e:
    logger.exception("foo failed")
    return DEFAULT_VALUE

Razão: log+throw causa erro logado N vezes (uma por layer).

Exceção: structured log com nível DEBUG em layer intermediária pra diagnóstico (não é mesmo nível que ERROR no boundary).

R5 — Sentinel errors / typed errors

Padrão Quando usar
Sentinel (var ErrNotFound = errors.New(...)) Erros que callers verificam por identidade
Typed (type NotFoundError struct{...}) Erros que carregam contexto estruturado (IDs, paths)
Wrapped opaque Erros que callers só propagam, sem inspecionar

Match na borda do boundary, não em cada layer:

// ✅ Boundary
if errors.Is(err, ErrNotFound) {
    return http.NotFound(w, r)
}
return http.InternalServerError(w, r, err)
# ✅ Boundary
try:
    handler()
except UserNotFoundError as e:
    return Response(404, str(e))
except DatabaseError:
    return Response(503, "db unavailable")
except Exception:
    logger.exception("unhandled")
    return Response(500, "internal error")

R6 — Resource cleanup on error

Recursos (file, lock, socket, transaction) devem ser liberados mesmo em erro. Padrões per-lang:

Linguagem Padrão canônico
Go defer file.Close() imediatamente após open
Rust RAII automático (Drop trait)
Python with open(...) as f: (context manager)
Dart try/finally ou await using (Dart 3.4+)
Java/Kotlin try-with-resources
Koda begin/ensure/end
JS/TS try/finally ou using (ES2024)

Nunca confiar em GC/finalizer pra liberar recurso crítico.

R7 — Error message format (internal)

Mensagens de log/erro internas seguem formato:

<operation>: <reason> [<context>]
loading user u-1234: timeout after 5s [host=db-01]
parsing config: invalid TOML at line 42
sending email to alice@example.com: connection refused

Lowercase no início (concatena bem com wrapping); presente do indicativo (não passado).

User-facing messages não seguem este formato — ver errors/user-facing-messages.kmd.

R8 — Error context hygiene

  • Não vazar PII em log de erro (emails, tokens, paths privados com username) — redact: user=u-1234 em vez de user=alice@x.com
  • Não vazar segredos (senhas, API keys) — redact com asterisco
  • Não vazar stack trace pra usuário externo (apenas log interno)

Cross-link com policies/security.kmd e errors/reporting.kmd (grupos A–G de privacidade).

R9 — Retry policy

Quando retry-able:

  • Network/IO timeouts — retry com exponential backoff + jitter
  • 5xx server errors — retry (idempotent ops only)
  • 429 rate limit — respeitar Retry-After header

Quando NÃO retry-able:

  • 4xx client errors (excerto 408/429) — bug do caller
  • Validation errors — input não-corrigível por retry
  • Authentication errors — credenciais não vão melhorar

Limite de retries: default 3; configurável via env. Após exausto, propagar erro.

R10 — Circuit breaker

Para chamadas externas com falha persistente (>50% em 1min), abrir circuit breaker (per-target, não global). Pattern: half-open periódico pra testar recovery.

Anti-patterns

AP-E1 — Catch generic Exception/Error

# ❌
try:
    foo()
except Exception:
    pass

# ✅
try:
    foo()
except (NetworkError, TimeoutError) as e:
    handle_network_failure(e)

Excerto: top-level boundary handler (com log + report).

AP-E2 — Empty catch (swallow)

# ❌ — silencia tudo
try:
    important()
except:
    pass

Se de fato é seguro ignorar, comentar por que:

# ✅
try:
    optional_telemetry_send()
except NetworkError:
    # telemetry é best-effort; falha de rede não bloqueia request principal
    pass

AP-E3 — Error message sem contexto

// ❌
return errors.New("failed")

// ✅
return fmt.Errorf("loading user %s: %w", userID, err)

AP-E4 — Panic em hot path

// ❌ — production panics em parse user input
let n: i32 = input.parse().unwrap();

// ✅
let n: i32 = input.parse().context("invalid integer input")?;

AP-E5 — Log + throw

Já coberto em R4.

Audit deterministic

error-handling-audit.sh:

  1. Catch generic Exception/Error em não-boundary → warning
  2. Empty catch (sem log nem comment) → error
  3. panic/unwrap/assert em production paths → warning
  4. Log seguido de raise no mesmo bloco → warning
  5. Resource open sem cleanup pattern → warning
  • errors/user-facing-messages.kmd — texto exibido ao usuário
  • errors/reporting.kmd — auto-report opt-in
  • code/comments.kmd — comentário em catch swallow obrigatório
  • policies/security.kmd — não vazar PII/secret em log

References