Error handling
dusk has no exception system and no panic. A function that can fail returns an error value alongside its result, and the compiler rejects any call site that does not handle that error. This page is the normative description of the error type, the fallible function shape, and the enforcement rules. For a task-oriented walkthrough, see the errors guide.
The error builtin type
Section titled “The error builtin type”error is a builtin type. It is not imported from any library.
An error carries a human readable message. Its representation is a pointer to the NUL terminated message text, and the empty, non-error value is a null pointer.
message: string, a human readable description read withtoString.
A numeric code and a source location are not part of the current representation. They may return in a later release.
Constructing errors
Section titled “Constructing errors”An error is built with struct literal syntax. error {} is the empty error, the value a fallible function returns on success.
return (0, error { message: "divide by zero" }) // a real errorreturn (a / b, error {}) // the empty errorMethods
Section titled “Methods”error has four methods.
| Method | Signature | Behavior |
|---|---|---|
exists | exists() -> bool | True if this is a real error, not an empty error. |
toString | toString() -> string | Formats the error as a string. |
check | check(handler: (error) -> void) -> void | If the error exists, calls handler with the error. Otherwise does nothing. |
ignore | ignore() -> void | Explicitly acknowledges and discards the error. |
toString() on the empty error is the empty string, not a null pointer, so printing an empty error is safe (since 0.2.6).
Fallible functions
Section titled “Fallible functions”Any function that can fail returns a tuple of (T, error).
func pop_back() -> (int32, error)Errors are always values. There is nothing to catch and nothing to unwind. A function with no meaningful result returns a bare error instead of a tuple (the builtin write_file(path, contents) -> error is one example), and the handling rules below apply to it the same way.
Handling errors
Section titled “Handling errors”Errors are values, so ordinary code handles them. Two shapes are common.
First, control flow that propagates an error upward. A lambda cannot return from its caller, so this shape uses exists.
y, e := x.pop_back()if e.exists() { printerr(e) return 1}Second, side effecting handling that logs and continues, using check.
y, e := x.pop_back()e.check(lambda (err: error) -> void { printerr(err)})A complete program showing all three handling forms:
// A fallible function returns (value, error). Every caller must handle it.func safe_div(a: int64, b: int64) -> (int64, error) { if b == 0 { return (0, error { message: "divide by zero" }) } return (a / b, error {})}
func main() -> int32 { // Propagate upward with exists and control flow. q, e := safe_div(10, 2) if e.exists() { printerr(e) return 1 } println(q)
// Log and continue with check. The value beside a real error is 0 here. bad, e2 := safe_div(1, 0) e2.check(lambda (err: error) -> void { printerr(err) }) println(bad)
// Explicit, greppable suppression with ignore. r, e3 := safe_div(9, 3) e3.ignore() println(r) return 0}Every error must be handled
Section titled “Every error must be handled”The tuple return is destructured at the call site. Both values must be bound to named variables, and the error binding must be used. Using an error means one of these things:
- inspecting it with
exists(), usually followed by control flow, - handling it with
check(...), - explicitly discarding it with
ignore(), - or returning it to the caller, which passes the obligation up.
y, e := x.pop_back()e.ignore() // explicit, visible, greppable suppressionUnlike Go, there is no _ suppression. ignore() replaces it. The difference is that ignore() is a visible, searchable acknowledgement in the source, while _ hides the decision.
Compile-time enforcement
Section titled “Compile-time enforcement”An unhandled error is a compile error, checked at two levels.
A bare statement that drops a fallible call’s result is rejected (since 0.2.5):
might_fail(1)// error: this expression's error result is ignored; bind it and// handle the error with exists, check, or ignoreA bound error that never reaches exists, check, or ignore, and is not returned to the caller, is also rejected (since 0.2.6). Printing the error does not count as handling it:
v, e := might_fail(1)printerr(e) // printing is not handlingprintln(v)// error: the error 'e' is never handled; inspect it with exists,// handle it with check, or discard it with ignoreReturning the error satisfies the rule, since the caller then faces the same obligation:
func parse_pair(b: int64) -> (int64, error) { if b == 0 { return (0, error { message: "zero input" }) } return (b * 2, error {})}
// Returning the error to the caller counts as handling it.func doubled(b: int64) -> (int64, error) { v, e := parse_pair(b) return (v, e)}
func main() -> int32 { v, e := doubled(21) if e.exists() { printerr(e) return 1 } println(v) return 0}The rule covers builtins too: read_file returns (string, error), spawn returns (thread, error), and join returns a bare error, so each caller resolves the failure through exists, check, or ignore. See builtins and concurrency for those signatures.
Related pages
Section titled “Related pages”- Errors guide: task-oriented patterns for working with errors.
- Functions: tuple returns and destructuring bindings.
- std.io:
printerrand the fallible read and parse functions.