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
- Escrever código que pode falhar
All triggers
- Implementar tratamento de erro em código novo
- Decidir entre exception vs return value
- Wrap/unwrap erro propagado entre layers
- Code review verificando error path
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-1234em vez deuser=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-Afterheader
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:
- Catch generic
Exception/Errorem não-boundary → warning - Empty catch (sem log nem comment) → error
panic/unwrap/assertem production paths → warning- Log seguido de raise no mesmo bloco → warning
- Resource open sem cleanup pattern → warning
Cross-link
errors/user-facing-messages.kmd— texto exibido ao usuárioerrors/reporting.kmd— auto-report opt-incode/comments.kmd— comentário em catch swallow obrigatóriopolicies/security.kmd— não vazar PII/secret em log
References
rfcs/design-RFC-001-koder-design-system.kmdspecs/errors/user-facing-messages.kmdspecs/errors/reporting.kmdspecs/code/comments.kmdpolicies/design-governance.kmd