Error Handling
Do provides structured error handling through the try/catch/finally
statement and the throw statement for raising errors.
Raising Errors
Use throw to raise an error with any value:
Try / Catch / Finally
The try statement executes a block and optionally catches errors and runs
cleanup code:
Basic Try/Catch
The result of a try statement is the result of the body on success, or the
result of the executed catch handler if an error was caught. Like if, a try
can be the right-hand side of let or an assignment:
Typed Catch Handlers
Errors can be caught by type. Handlers are tried in order:
import std:
- TypeError
- IndexError
try
something()
catch TypeError: err
echo "Type error: $err"
catch IndexError: err
echo "Index error: $err"
catch err
echo "Other error: $err"
Typed handlers use subtype matching, so a handler for a parent class will catch errors of any child class. The catch-all handler (without a type) must be last and catches any error not matched by a preceding handler.
Finally
The finally block runs regardless of whether the body succeeded or raised an
error. It runs after all other blocks but before any error is propagated:
If the finally block itself raises an error, it takes priority over any
error from the body or catch handlers.
Combining Catch and Finally
All three clauses can be used together:
import std:
- TypeError
try
let conn = connect()
query $conn
catch TypeError: err
echo "Bad query: $err"
nil
catch err
echo "Connection failed: $err"
nil
finally
disconnect()
Error Types
The std error types form a hierarchy rooted at Error:
Error
├── AbortError
├── CanceledError
└── RuntimeError
├── TypeError
├── ValueError
├── ShapeError
│ ├── MissingItemError
│ │ ├── MissingKeyError
│ │ └── MissingPosError
│ └── UnexpectedItemError
│ ├── UnexpectedKeyError
│ └── UnexpectedPosError
├── DataError
└── FieldError, IndexError, StateError, ...
RuntimeError is the supertype of every failure a program
might expect to recover from. AbortError and
CanceledError sit outside it, so catch RuntimeError
never swallows them.
TypeError, ShapeError, and
ValueError say what is wrong with a value a caller passed:
its kind, its items (missing or unexpected, in a call's arguments or a
destructured value), or its contents. Failures like these are bugs in the
calling code, mostly caught by type checking, and are seldom worth catching.
A failure that type checking can't rule out needs a type callers can catch:
DataErrorwhen an external document or stream is invalid. Module parse and decode errors subclass it. It is a sibling ofValueError, not a subtype.- Otherwise a module's own subtype of
RuntimeError.
The distinction between ValueError and DataError follows what the operation
is for. Bad contents in what it exists to parse or decode raise DataError, as
Int "abc" does. Bad options or settings that control how it works raise
ValueError, as indent: -1 does.
Subclassing Error Types
A module that wants callers to be able to catch its failures specifically can
define its own error class. Inherit from the closest std error type and chain
to its constructor, passing up the message the error should display:
import std:
- RuntimeError
- ValueError
class NoDomainError: RuntimeError
pub field name = nil
def (init) self name
RuntimeError.(init) $self "domain does not exist: $name"
self.name = name
The chained call is what gives the instance its string form, so the subclass
needs no (str) of its own. Subtype matching then works the whole way up the
chain — a handler for the specific type, for the std type it inherits from, or
for Error will all catch it:
Inherit from the type that describes the failure — DataError for a malformed
document, RuntimeError when nothing more specific fits. Every concrete std
error type is subclassable. AbortError can be neither
subclassed nor constructed, and neither can the abstract ShapeError,
MissingItemError, and UnexpectedItemError; subclass one of their concrete
subtypes instead.