Errors are values
When a call can fail, the failure comes back out of it.
A function that can fail says both outcomes in its return type. parse returns
i64 or ParseError: a number, or a reason it isn’t one. match takes both:
func years_from(text: string) -> string {
let n: i64 or ParseError = text.parse<i64>()
match n {
i64 as years => return "{years} years"
ParseError as e => return "not a number: {e.message()}"
}
}
One arm per outcome, each naming what it got.
Every error type has a message(), so e.message() works whatever kind of error arrived.
The annotation on n is there to show the type. You’d normally leave it off.
catch writes the error arm
Most of the time the error arm is just “use this value instead”. catch is that arm on its
own, and the success value comes straight out:
func retries_from(text: string) -> i64 {
return text.parse<i64>() catch _ => 3
}
The binder is required. catch e => uses the error, catch _ => throws it away. There is no
catch 3:
error[E0100]: `catch` needs a binder
--> docs/book/errors/errors-are-values/bare_catch.rk:11:31
|
11 | let n = text.parse<i64>() catch 3
| ^^^^^ here
= fix: write `catch e => value` to use the error, or `catch _ => value` to
drop it — there is no bare-value form, so an error is never swallowed
silently
= why: a fallback is the one form here that can destroy information. `??`
discards a `none`, which carried nothing; `catch` discards or
transforms an error, which carried a payload someone should look at.
So the binder is what makes the second one legible — `catch _ =>` is
the spelling for "an error dies here", and it greps [type.errors/ER14]
A catch body can also leave instead of producing a value — catch e => return wrap(e) turns
the error into your own and hands it to the caller.
try writes the commonest one
The error arm people write most is “hand it to my caller unchanged”. try r does what
r catch e => return e does:
func doubled(text: string) -> i64 or ParseError {
let n = try text.parse<i64>()
return n * 2
}
doubled can’t swallow the failure, so its own return type carries it: i64 or ParseError.
The error has to go somewhere, so the function needs an error branch to put it in. This one doesn’t have one:
func report(text: string) {
let port = try port_of(text)
println("{port}")
}
error[E0316]: `try` requires function returning Result or Option, found `void`
--> docs/book/errors/errors-are-values/try_without_a_carrier.rk:30:16
|
30 | let port = try port_of(text)
| ^^^^^^^^^^^^^^^^^ try used here
= fix: change the function return type to `T or E`
= why: `try` propagates errors upward — the enclosing function must declare
an error type in its return
! stops the program
The last thing the error arm can do is give up. ! takes the value out, and panics if the
error came back instead:
func main() {
let port = "eighty-eighty".parse<i64>()!
println("listening on {port}")
}
panic at docs/book/panics/errors-are-values/force_on_an_error.rk:10: ! on a value that was an error: non-numeric character
A panic is not an error value. It has no type, it never appears in a return type, and a caller
can’t handle it. The task dies, its ensure cleanups run on the way out, and any locks it held
are released.
Use ! where there is no sensible way to carry on — a config the program can’t start without.
For anything the caller could deal with, hand it back with try.
Your own error type
ParseError came with parse. For your program’s own failures, write an enum and give it a
message():
enum ConfigError {
Missing(string)
Malformed(string)
}
ConfigError implements Error {
func message(self) -> string {
match self {
ConfigError.Missing(key) => return "{key} is not set"
ConfigError.Malformed(key) => return "{key} is not a number"
}
}
}
Then it goes on the right of or:
func port_from(text: string) -> i64 or ConfigError {
if text == "" {
return ConfigError.Missing("port")
}
let n = text.parse<i64>() catch _ => {
return ConfigError.Malformed("port")
}
return n
}
Running it
The code above is one program, and it prints:
41 years
not a number: non-numeric character
retries 5
retries 3
doubled 82
port 8080
bad config: port is not set
bad config: port is not a number
Rules behind this page
- Error types:
T or E,try,catch, and theErrorinterface - Panics: the other failure channel, for bugs rather than expected outcomes