Roc has two ways for something to go wrong:
- A
Try is a value that's either a success (Ok) or a failure (Err). Most errors are represented this way, and the type system makes sure they get handled. - A crash stops the program. This is for situations where there's no reasonable way to continue, like a bug that has put the program in a state it was never supposed to be in.
Roc doesn't have exceptions. A function's type always tells you how it can fail (other than by
crashing), because the ways it can fail are part of its return type.
Try
Try is a nominal tag union with two tags:
Try(ok, err) := [Ok(ok), Err(err)]
A function that might fail returns a Try. For example, List.first returns a
Try(item, [ListWasEmpty]), since an empty list has no first element:
[1, 2, 3].first() # Ok(1)
[].first() # Err(ListWasEmpty)
Since a Try is an ordinary value, you handle it the same way you'd handle any other tag union,
for example with a match:
describe = |list|
match list.first() {
Ok(first) => "It starts with ${first}"
Err(ListWasEmpty) => "It's empty"
}
Error Types Are Tag Unions
Notice that the error type above is [ListWasEmpty]: a structural tag union
with one tag. That's the usual way to write error types in Roc, and it works especially well with
the ? operator, because structural tag unions can be combined:
parse_and_double : Str -> Try(U64, [BadNumStr, Overflow])
parse_and_double = |str| {
n = U64.from_str(str)? # can fail with BadNumStr
doubled = n.times_try(2)? # can fail with Overflow
Ok(doubled)
}
Here, the two ?s can return different errors, and the function's error type is the union of
both: [BadNumStr, Overflow]. If you leave off the type annotation, the compiler infers that for
you. You don't need to declare an error type that wraps both of them, or convert one into the
other.
And since the error type lists every error that can happen, a match on the result knows exactly
which errors to handle, and gives an exhaustiveness error if
you forget one.
Handling Errors
There are several ways to handle a Try, depending on what you want to do with the error.
Returning Errors Early (with ?)
Writing ? after an expression that evaluates to a
Try gives you the Ok payload if it's Ok. If it's Err, the function immediately returns that
Err:
x = U64.from_str(a)?
This is the right choice when the caller is in a better position to decide what to do about the
error.
If you'd like to change the error before it gets returned (for example, to say which input was
bad), put a function or a tag after the ?, with spaces around it. See
? with a Handler:
x = U64.from_str(a) ? |_| InvalidFirst # returns Err(InvalidFirst)
y = U64.from_str(b) ? InvalidSecond # returns Err(InvalidSecond(BadNumStr))
Using a Default Value (with ??)
?? gives you the Ok payload, or a default value if it's Err:
first = list.first() ?? 0
The Try module has functions for working with Try values without unwrapping them:
| Function | What it does |
|---|
map_ok | Transforms the Ok payload, leaving Err alone |
map_err | Transforms the Err payload, leaving Ok alone |
on_err | Runs a function on the Err payload which returns another Try, for example to try something else |
ok_or | Gives the Ok payload, or a default value (like ??) |
is_ok, is_err | Returns whether it's Ok or Err |
Each of these that takes a function also has a version ending in ! (like map_ok!) which takes an
effectful function.
Crashing
The crash statement stops the program, with a message:
if index >= len {
crash "This should never happen: index ${index.to_str()} was past the end."
}
What happens after a crash is up to the platform. Some platforms exit the program,
whereas others (such as a web server) might recover by returning an error response and moving on
to the next request.
A few builtin operations crash too, when there's no sensible value they could return:
- Integer overflow, like
255.U8 + 1 - Integer division by zero
- Running out of memory
Each of these has a non-crashing alternative when you want to handle the problem yourself. For
example, a.plus_try(b) returns Err(Overflow) instead of crashing.
When to Crash
Use Try for things that can go wrong even when the program is working correctly: a file that
doesn't exist, user input that isn't a valid number, a network request that times out, and so on.
The person calling your function needs to know these can happen, and Try makes sure they do.
Use crash for things that should be impossible if the program is correct. Returning a Try for
these would force every caller to write code for a situation that can't happen, and there would
be no good way for that code to handle it anyway.
A crash during compile-time evaluation is reported as a compile-time error, so a
crash in code that only depends on compile-time constants never reaches your users.
A Try is an ordinary tag union. It's stored inline, it never
allocates, and creating one or checking which tag it has is about as cheap as it gets. ? and ??
compile to a single check of the tag, followed by a jump.
So there's no performance reason to avoid Try in favor of crashing. In particular, unlike
throwing an exception in some languages, returning an Err costs the same as returning an Ok.
One thing to watch out for: since a tag union takes up as much space as its largest payload, a
large error payload makes every Try of that type large, including the Oks. If an error payload
is much bigger than the Ok payload, and the function is called a lot, consider putting the
error's details in a Box.