Errors, Option and Result
A function can fail to give an answer. A search can find nothing, and a parse can meet a bad
character. Vx has no exceptions. A function that can fail says so in its return type, with
Option or Result. This chapter shows how to make these values, how to read them, and how to
pass an error up to the caller.
Each program in this chapter is also a test of the compiler. You can find it under
tests/backend/pass/ with the name that its section gives.
Option: a value or nothing
Option<T> holds either one value of type T (Some) or no value (None). It comes from
import core::option;. Write a variant with its full type: Option<i64>::Some(3) and
Option<i64>::None(). Vx for Rust programmers lists this and the
other differences from Rust.
Read an Option with match, as Control flow shows for an enum. Each
arm handles one variant. A method is shorter when you need only one fact:
o.is_some()ando.is_none()tell which variantois. They borrowo, so you can still use it after the call.o.unwrap_or(d)gives the value, ordfor aNone.
This program searches for the first square above a limit (book_errors_find_a_value.vx):
import core::option;
fn first_square_above(limit : i64) -> Option<i64> {
for i in 1..10 {
let sq = (i * i) as i64;
if sq > limit {
return Option<i64>::Some(sq);
}
}
return Option<i64>::None();
}
fn report(limit : i64) -> void {
match first_square_above(limit) {
Option<i64>::Some(sq) => {
println!("first square above {} is {}", limit, sq);
}
Option<i64>::None() => {
println!("no square above {}", limit);
}
}
}
fn main() -> i32 {
report(20);
report(200);
let missing = first_square_above(500);
println!("found {}", missing.is_some());
println!("or 0 gives {}", missing.unwrap_or(0));
return 0;
}
It prints:
first square above 20 is 25
no square above 200
found false
or 0 gives 0
Result: a value or an error
Result<T, E> holds either a value of type T (Ok) or an error of type E (Err). It comes
from import core::result;. Use it when the caller must know why the operation failed. The error
type is often an enum of your own, with one variant for each way to fail.
r.is_ok() and r.is_err() tell which variant r is. They also borrow r, so they work for a
payload that is not Copy. r.unwrap_or(d) gives the value, or d for an Err.
The ? operator
r? gives the value of an Ok. For an Err, it returns that Err from the function at once.
The function must return a Result with the same error type. So ? replaces a match that only
passes the error up.
? does not convert the error. If the error types differ, the compiler reports E3063. Use
r.map_err(f) first: it calls the closure f on the error of an Err, and it keeps an Ok as it
is. Write the type that the closure returns, as in | c : i64 | -> AddError { .. }. Without it,
the ? after map_err gets a wrong E3063 (Vx#3233).
In this program, digit reports a bad character with its code, an i64. two_digits converts
that code to an AddError and passes it up with ? (book_errors_question_mark.vx):
import core::result;
enum AddError {
NotADigit(i64),
TooLarge(i64),
}
// The character code of a digit, such as 50 for '2', gives its value.
fn digit(c : i64) -> Result<i64, i64> {
if c >= 48 && c <= 57 {
return Result<i64, i64>::Ok(c - 48);
}
return Result<i64, i64>::Err(c);
}
// A number with two digits, from the codes of its characters.
fn two_digits(tens : i64, ones : i64) -> Result<i64, AddError> {
let t = digit(tens).map_err(| c : i64 | -> AddError {
AddError::NotADigit(c)
})?;
let o = digit(ones).map_err(| c : i64 | -> AddError {
AddError::NotADigit(c)
})?;
return Result<i64, AddError>::Ok(t * 10 + o);
}
fn add(a : Result<i64, AddError>, b : Result<i64, AddError>) -> Result<i64, AddError> {
let sum = a? + b?;
if sum > 99 {
return Result<i64, AddError>::Err(AddError::TooLarge(sum));
}
return Result<i64, AddError>::Ok(sum);
}
fn report_error(e : AddError) -> void {
match e {
AddError::NotADigit(c) => {
println!("error: the character {} is not a digit", c);
}
AddError::TooLarge(n) => {
println!("error: the sum {} is too large", n);
}
}
}
fn report(r : Result<i64, AddError>) -> void {
match r {
Result<i64, AddError>::Ok(sum) => {
println!("sum {}", sum);
}
Result<i64, AddError>::Err(e) => {
report_error(e);
}
}
}
fn main() -> i32 {
// The codes 49 and 50 are '1' and '2'. The code 120 is 'x'.
report(add(two_digits(49, 50), two_digits(51, 48)));
report(add(two_digits(49, 120), two_digits(51, 48)));
report(add(two_digits(53, 52), two_digits(53, 52)));
return 0;
}
It prints:
sum 42
error: the character 120 is not a digit
error: the sum 108 is too large
A pattern in a match cannot test a variant inside a payload. Err(AddError::TooLarge(n)) gets
the error E3050. So report binds the error to e, and report_error matches on e.
? takes only a Result. A ? on an Option gets the error E3063. Convert the Option first:
o.ok_or(e) gives Ok with the value, or Err(e) for a None.
fn quarter(n : i64) -> Result<i64, i64> {
let h = half(n).ok_or(n)?;
let q = half(h).ok_or(h)?;
return Result<i64, i64>::Ok(q);
}
A ? in a task or a tasks block passes the error to the block, not to the function.
Tasks and channels shows this.
Take a value out with take and replace
A field of type Option can hold a value for a time. o.take() moves the value out and leaves
None in o. o.replace(v) puts v in o and gives the old contents. Both borrow o mutably,
so o must be a let mut variable or a field of one. Both work for a value that is not Copy.
In this program, a desk holds at most one ticket (book_errors_take_and_replace.vx). A message
such as "took" is a *const i8, the type of a string literal:
import core::option;
// A struct with no `Copy` impl. A move of a ticket leaves nothing behind.
struct Ticket {
id : i64,
}
struct Desk {
waiting : Option<Ticket>,
}
fn show(what : *const i8, o : Option<Ticket>) -> void {
match o {
Option<Ticket>::Some(t) => {
println!("{} ticket {}", what, t.id);
}
Option<Ticket>::None() => {
println!("{} nothing", what);
}
}
}
fn main() -> i32 {
let mut desk = Desk {
waiting: Option<Ticket>::None()
};
show("replaced", desk.waiting.replace(Ticket {
id: 1
}));
show("replaced", desk.waiting.replace(Ticket {
id: 2
}));
show("took", desk.waiting.take());
println!("empty {}", desk.waiting.is_none());
return 0;
}
It prints:
replaced nothing
replaced ticket 1
took ticket 2
empty true
Errors that the program cannot handle
Some errors mean that the program has a bug. Then the best action is to stop the program with a
clear message. A tour of Vx explains panic, abort and -> !.
o.unwrap()andr.unwrap()give the value. For aNoneor anErr, they stop the program.o.expect(msg)andr.expect(msg)do the same, but they printmsg.panic(msg)stops the program withmsg.abort()stops it with no message.
Each of these prints the file and line of the call, not a line in the standard library. Use
expect rather than unwrap, so that the message tells the reader what went wrong.
This program divides twice. The second division fails (book_errors_expect_stops_the_program.vx):
import core::result;
fn divide(a : i64, b : i64) -> Result<i64, i64> {
if b == 0 {
return Result<i64, i64>::Err(a);
}
return Result<i64, i64>::Ok(a / b);
}
fn main() -> i32 {
let good = divide(10, 2);
println!("ok {}, err {}", good.is_ok(), good.is_err());
println!("10 / 2 = {}", good.unwrap());
let bad = divide(10, 0);
let q = bad.expect("the divisor must not be zero");
println!("never printed {}", q);
return 0;
}
It prints the lines below and ends with an error status. The last line names the line of the
expect call.
ok true, err false
10 / 2 = 5
panic: the divisor must not be zero
at book_errors_expect_stops_the_program.vx:36
unwrap and expect take the Option or Result by value. So call is_ok before them, as this
program does, and not after.
More methods
core/option.vx and core/result.vx in the standard library have more methods, each with a
comment. Some of them are map, and_then, or, filter, ok, err, unwrap_err and
unwrap_or_else.