Errors as values
dusk has no exception system and no panic. A function that can fail says so in its return type, hands the failure back as an ordinary value, and the compiler makes the caller face it. This guide covers the error builtin type, fallible function signatures, the must-handle rule, and the handling patterns you will use every day.
The error type
Section titled “The error type”error is a builtin type. It is not imported from any library. It carries one piece of data:
message: string: a human readable description, read withtoString.
Under the hood an error is a pointer to the NUL terminated message text, and the empty, non-error value is a null pointer. A numeric code and a source location are not part of the current representation; they may return in a later release.
The type has four methods:
| Method | Signature | Meaning |
|---|---|---|
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 it. Otherwise does nothing. |
ignore | ignore() -> void | Acknowledges the error and throws it away. |
You construct an error with struct-literal syntax. error { message: "..." } builds a real error, and error {} builds the empty one that means success:
return (0, error { message: "divide by zero" }) // failurereturn (a / b, error {}) // successtoString on the empty error is the empty string, so printing an error you have not checked first cannot crash. It just prints nothing.
Fallible functions
Section titled “Fallible functions”Any function that can fail returns a tuple of (T, error):
func pop_back() -> (int32, error)The caller destructures the tuple at the call site and both values must be bound to named variables. Here is a complete program that defines a fallible function and handles both outcomes:
// A fallible function returns (value, error). The caller decides what// failure means.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 { q, e := safe_div(10, 2) if e.exists() { printerr(e) return 1 } println(q)
bad, e2 := safe_div(1, 0) if e2.exists() { printerr(e2) return 1 } println(bad) return 0}printerr is a builtin that works like println but writes to stderr, which is the natural place for error reports.
Every error must be handled
Section titled “Every error must be handled”The error binding must be used, and using an error means one of exactly three things:
- inspecting it with
exists(), usually followed by control flow, - handling it with
check(...), - or explicitly discarding it with
ignore().
Leave an error binding untouched and the program does not compile:
a, e := might_fail(1) // e never used belowprintln(a)error: unused variable 'e'error: the error 'e' is never handled; inspect it with exists, handle it with check, or discard it with ignoreUnlike Go, there is no _ suppression. ignore() replaces it, and unlike _ it leaves a visible, searchable mark in the source. When you audit a codebase for swallowed errors, grep ignore finds every one.
Handling patterns
Section titled “Handling patterns”Branch on exists
Section titled “Branch on exists”The first common shape is control flow that propagates the failure upward. A lambda cannot return from its caller, so this shape uses exists and an ordinary if:
y, e := x.pop_back()if e.exists() { std.io.printerr(e) return 1}This is the propagation pattern: report or wrap the error, then return early. Code after the if runs only on success.
Log and continue with check
Section titled “Log and continue with check”The second shape is side-effecting handling that logs and continues. check takes a lambda and calls it only when the error exists:
func might_fail(n: int64) -> (int64, error) { if n < 0 { return (0, error { message: "negative input" }) } return (n * 2, error {})}
func main() -> int32 { // check: log and continue. a, e1 := might_fail(-5) e1.check(lambda (err: error) -> void { printerr(err) }) println(a)
// ignore: explicit, visible suppression. b, e2 := might_fail(21) e2.ignore() println(b) return 0}Note that check cannot alter control flow in the enclosing function. The lambda’s return leaves the lambda, not the caller. Use it when the right response to failure is a side effect, not an early exit.
Discard with ignore
Section titled “Discard with ignore”When failure genuinely does not matter, say so:
y, e := x.pop_back()e.ignore() // explicit, visible, greppable suppressionThis satisfies the must-handle rule while leaving a record of the decision in the source.
Errors in practice
Section titled “Errors in practice”The standard library follows the same convention everywhere. parse_float and parse_int_radix in std.string return the parsed value with an error, so a bad parse is caught, not guessed:
@import std.string
// parse_float returns (float64, error); a bad parse is caught, not guessed.func main() -> int32 { f, fe := parse_float("3.5") if fe.exists() { printerr(fe) return 1 } println(f + 0.5)
g, ge := parse_float("bad") if ge.exists() { println(-1) } else { println(g) } return 0}Sometimes an error is not a defect at all but a normal condition. read_line in std.io returns (string, error), and the error exists at end of input, which is how a read loop knows to stop:
@paradigm procedural
@import std.io
// read_line returns (string, error); the error exists at end of input.// Read lines until end of input and count them.func main() -> int32 { mut count: int64 = 0 mut done: bool = false while !done { line, e := read_line() if e.exists() { done = true } else { print_line(line) count = count + 1 } } print_int(count) return 0}This file declares @paradigm procedural because it uses mut and while; the error handling itself needs no paradigm. See Paradigms for how the directives work.
A function whose only result is success or failure returns a bare error rather than a tuple, and the same must-handle rule applies to the single binding:
werr := write_file("/tmp/out.txt", "persisted")werr.ignore()
je := join(t) // join(t: thread) -> errorje.ignore()Where to go next
Section titled “Where to go next”- Error handling reference: the precise rules in specification form.
- Standard library: io and string: the fallible functions used above.
- Concurrency:
spawn,join, and channels report failure through the sameerrortype.