Exceptions [errors.exceptions]
In general, prefer explicit error handling mechanisms.
Annotate each module at top-level (before imports):
{.push raises: [].}
Use explicit {.raises.}
annotation for each public (*
) function.
Raise Defect
to signal panics and undefined behavior that the code is not prepared to handle.
# Enable exception tracking for all functions in this module
`{.push raises: [].}` # Always at start of module
# Inherit from CatchableError and name XxxError
type MyLibraryError = object of CatchableError
# Raise Defect when panicking - this crashes the application (in different ways
# depending on Nim version and compiler flags) - name `XxxDefect`
type SomeDefect = object of Defect
# Use hierarchy for more specific errors
type MySpecificError = object of MyLibraryError
# Explicitly annotate functions with raises - this replaces the more strict
# module-level push declaration on top
func f() {.raises: [MySpecificError]} = discard
# Isolate code that may generate exceptions using expression-based try:
let x =
try: ...
except MyError as exc: ... # use the most specific error kind possible
# Be careful to catch excpetions inside loops, to avoid partial loop evaluations:
for x in y:
try: ..
except MyError: ..
# Provide contextual data when raising specific errors
raise (ref MyError)(msg: "description", data: value)
Pros
- Used by
Nim
standard library - Good for quick prototyping without error handling
- Good performance on happy path without
try
- Compatible with RVO
Cons
- Poor readability - exceptions not part of API / signatures by default
- Have to assume every line may fail
- Poor maintenance / refactoring support - compiler can't help detect affected code because they're not part of API
- Nim exception hierarchy unclear and changes between versions
- The distinction between
Exception
,CatchableError
andDefect
is inconsistently implemented Defect
is not tracked
- The distinction between
- Without translation, exceptions leak information between abstraction layers
- Writing exception-safe code in Nim impractical due to missing critical features present in C++
- No RAII - resources often leak in the presence of exceptions
- Destructors incomplete / unstable and thus not usable for safe EH
- No constructors, thus no way to force particular object states at construction
ref
types incompatible with destructors, even if they worked
- Poor performance of error path
- Several heap allocations for each `Exception`` (exception, stack trace, message)
- Expensive stack trace
- Poor performance on happy path
- Every
try
anddefer
has significant performance overhead due tosetjmp
exception handling implementation
- Every
Practical notes
The use of exceptions in Nim has significantly contributed to resource leaks, deadlocks and other difficult bugs. The various exception handling proposals aim to alleviate some of the issues but have not found sufficient grounding in the Nim community to warrant the language changes necessary to proceed.
Defect
Defect
does not cause a raises
effect - code must be manually verified - common sources of Defect
include:
- Over/underflows in signed arithmetic
[]
operator for indexing arrays/seqs/etc (but not tables!)- accidental/implicit conversions to
range
types
CatchableError
Catching CatchableError
implies that all errors are funnelled through the same exception handler. When called code starts raising new exceptions, it becomes difficult to find affected code - catching more specific errors avoids this maintenance problem.
Frameworks may catch CatchableError
to forward exceptions through layers. Doing so leads to type erasure of the actual raised exception type in raises
tracking.
Open questions
- Should a hierarchy be used?
- Why? It's rare that calling code differentiates between errors
- What to start the hierarchy with? Unclear whether it should be a global type (like
CatchableError
orValueError
, or a module-local type
- Should exceptions be translated?
- Leaking exception types between layers means no isolation, joining all modules in one big spaghetti bowl
- Translating exceptions has high visual overhead, specially when hierachy is used - not practical, all advantages lost
- Should
raises
be used?- Equivalent to
Result[T, SomeError]
but lacks generics - Additive - asymptotically tends towards
raises: [CatchableError]
, losing value unless exceptions are translated locally - No way to transport accurate raises type information across Future/async/generic code boundaries - no
raisesof
equivalent oftypeof
- Equivalent to
Background
- Stew EH helpers - Helpers that make working with checked exceptions easier
- Nim Exception RFC - seeks to differentiate between recoverable and unrecoverable errors
- Zahary's handling proposal - seeks to handle any kind of error-generating API
- C++ proposal - After 25 years of encouragement, half the polled C++ developers continue avoiding exceptions and Herb Sutter argues about the consequences of doing so
- Google and llvm style guides on exceptions