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 it directly ase.messageor format the whole error withtoString.
Reading e.message gives the message text for a real error and "" for the empty error, so it is always safe to read. The field is read only (since 0.5.4). Assigning to it, e.message = "...", is rejected, an error's message is read only; build a new error instead, and message is the only field an error carries, so any other name is rejected, error has no field '<name>'; it carries only 'message'.
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).
An error does not compare with == or !=. Since 1.2.0 comparison closed on a named list of types, and error is not one of them, so e == f and e != f are both rejected at check, an error does not compare; test it with exists(). The pointer identity such a compare would read says nothing about whether an error is real, which is the question you actually want answered. Ask it with exists(), and compare an error’s text by reading e.message on each side, since a string compares by content. See Comparison for the full comparable-type list.
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. Binding the whole pair to one name, r := x.pop_back(), is rejected (since 0.5.4), a fallible result must be destructured; bind the value and the error, so the error can never hide unread inside the pair. The error binding must then 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(), - returning it to the caller, which passes the obligation up,
- or handing it directly to another function’s
errorparameter, which passes the obligation to that function.
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.
Errors passed to a parameter
Section titled “Errors passed to a parameter”The obligation follows an error into a parameter (since 0.5.3). A function that takes an error parameter must handle it, exactly as a bound error must. An empty body that receives an error and drops it is rejected:
func swallow(err: error) -> void { }// error: the error 'err' is never handled; inspect it with exists,// handle it with check, or discard it with ignoreThe callee discharges its own error parameter with the same menu: inspect it with exists(), resolve it with check(...), discard it with ignore(), return it, or hand it off to yet another error parameter.
This is also what lets a hand-off discharge the caller. Handing a bound error straight to an error parameter counts as handling it, but the hand-off must be direct: the error has to be the argument itself. Reading it into a fresh value first, or passing it through a passthrough helper, does not count, so sink(fst(e, e2)) still leaves both e and e2 unhandled even though sink takes an error. Passing an error to a parameter that is not itself typed error discharges nothing.
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.