name: nim-error-handling
description: Design clear Nim error-handling flows; when to raise exceptions vs return Option/bool, how to define raises contracts, and where to translate or record failures. Use when reviewing failure behavior, parse errors, exception boundaries, or batch processing that needs per-item error reporting.
Nim Error Handling
Use this skill to decide where code should raise, catch, translate, or return structured failure data.
Rules
Choose the Failure Channel
- Raise exceptions for invalid input, semantic/validation failure, and operational/runtime failure (I/O, OS, network).
- Return
boolfor probe-style queries where failure means “not found / condition not met” and no value payload is needed. - Return
Option[T]when the absent value is a value type with no natural sentinel and the caller benefits from composable operations likemaporflatMap. - For scanning/parsing helpers, return consumed length as
int(0= no match) and write the parsed value via avarout-parameter. - Use structured result objects only at the orchestrator/batch boundary, where exceptions are converted into per-item outputs.
- Do not pass ad-hoc
(ok, kind, message)step-result objects through straight-line internal flows.
Place Boundaries
- Internal step procs should raise (do not catch in the same layer).
- Catch only to recover, translate at a boundary, record failure, or clean up.
- Keep the success path straight-line between boundaries.
- Do not wrap each raising call in its own local
try/except. - Validate at boundaries where data enters from outside the typed flow: files, network, user input, config text, FFI, and public API inputs.
- Add fallback logic only when the caller has a useful recovery path. If failure only leaves stale or unusable state, report the boundary error plainly.
Choose Exception Types
- Catch
CatchableErroras the recoverable catch-all. Do not catch bareException. - Use specific existing exception types such as
ValueError,IOError, andOSErrorwhen callers should distinguish them. - Add a custom exception type only when callers need a narrower semantic type for distinct handling.
- Custom exception types must derive from
CatchableError(recoverable) orDefect(programming bugs). - Inherit from a more specific existing base like
ValueErrororIOErrorwhen the semantic fit is clear. - Deriving directly from
CatchableErrororDefectis fine when no intermediate base matches.
Make Contracts Explicit
- Add explicit
raisescontracts on exported procs only when the exception surface is stable and narrow. - Do not annotate every internal helper by default.
- Use
.raises: []for a proc that must not raise. - Use
.raises: [X]when one specific exception type is part of the contract. - Treat
raisesas a compiler-checked contract, not documentation prose.
Translate and Inspect Errors
- Translate low-level errors only at module/subsystem boundaries.
- Add local context and preserve the underlying reason in the new message (include the original exception message).
- If the handler only needs the message text, use
getCurrentExceptionMsg(). - If the handler needs the exception object or fields, use
except X as eorgetCurrentException().
Cleanup
- Use
try/finallyfor cleanup.
Workflow
- Decide whether failure is expected.
If it is an expected miss, return
bool,Option, or a parse-length value. Do not throw. - Mark the boundaries. Step procs raise. Parse helpers may catch once. Module boundaries may translate. Orchestrators may record per-item failure.
- Pick the exception type. Start with an existing type. Add a subtype only if callers need distinct handling.
- Write the contract.
Add
raiseson exported procs when the exception surface is narrow and stable. Keep it accurate. - Verify the shape.
Compile the code. Run the repo tests. If you wrote
raises, ensure the compiler accepts the contract.
Minimal Pattern
import std/[options]
proc findConfig*(paths: seq[string]): Option[string] =
for p in paths:
if fileExists(p):
return some(p)
none(string)
proc loadConfig*(path: string): Config =
if path.len == 0:
raise newException(ValueError, "config path is empty")
try:
result = parseConfig(readFile(path))
except IOError:
raise newException(IOError, "cannot read config: " & getCurrentExceptionMsg())
Common Mistakes
| Mistake | Why it is wrong |
|---|---|
| Catching in every layer | Hides the real boundary and makes failure flow harder to follow |
| Throwing for an expected miss | Turns normal control flow into exception flow |
Passing ok/kind/message objects between internal steps |
Reimplements exception propagation with more boilerplate |
Catching bare Exception |
Also catches Defect, which is not recoverable application flow |
| Adding a custom exception type with no distinct handling | Adds type noise without changing the contract |
Using try/except for cleanup |
Cleanup belongs in finally |
| Adding fallback logic without a useful recovery path | Preserves stale or unusable state and obscures the real boundary failure |
References
references/batch_preview_boundary.md— Batch boundary that records per-item failures