Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Errors

An error is raised with raise(e) and handled with with h { … } around the code that may raise it. There is one model: no Result type threaded by hand, no ?, no exceptions separate from the rest of the language. raise is an operation of an ordinary effect, Fail[E], and its handlers are ordinary handlers (Effects and handlers has the general mechanism).

Raising and handling

include fail declares the effect and three handlers:

handleranswers
with to_result { e }inl the value of e, or inr the error
with default(d) { e }the value of e, or d
with map_err(f) { e }the value of e; an error x is raised again as f(x), to the handler around it

A function that raises says so after its result type, with Fail[E], and a function that calls one that raises raises too, with nothing written: its row, the effects it may perform, is inferred. A call of a function that raises needs no ?: the error goes on by itself to the nearest handler.

include textio
include fail

-- The sum of the elements; a negative one raises its index.
fn checked<'a>(v: array_imm 'a i32) -> i32 with Fail[i32] =
  let mut s = 0;
  let mut i = 0;
  for p in v { let x = *p; if x < 0 { raise(i) }; s = s + x; i = i + 1 };
  s

-- A function that calls one that raises raises too: its row is inferred.
fn twice<'a>(v: array_imm 'a i32) -> i32 = checked(v) * 2

fn report(r: i32 + i32) -> 1 =
  match r {
    inl n -> print_lit("sum "); print_dec(n)
  | inr i -> print_lit("negative at "); print_dec(i)
  };
  print_byte(b'\n')

fn good(i: i32) -> i32 = i * 10
fn bad(i: i32) -> i32 = if i == 2 { 0 - 1 } else { i }

main -> 1 =
  let a = array_new[i32](3, good);
  let b = array_new[i32](3, bad);
  let x = borrow imm a { v -> with to_result { twice(v) } };
  let y = borrow imm b { v -> with to_result { twice(v) } };
  let z = borrow imm b { v -> with default(0) { twice(v) } };
  report(x);
  report(y);
  print_dec(z);
  print_byte(b'\n')
-- output: sum 60
-- output: negative at 2
-- output: 0

A raise that no handler can reach is a compile-time error naming the function that performs it (`f` performs `Fail`, but no handler of `Fail` is in scope here): an error never escapes main unnoticed. The one exception is division, below.

If you do write the row, the compiler holds you to it: performing an effect the row leaves out is an error. A row may also give the error’s type where nothing else does.

What happens to values when an error leaves

An error unwinds: what the code it leaves owned is freed by its derived consumes, and every borrow it was inside ends, the place going back to its owner. Here the cell that checked holds when it raises is freed on the way out, and the run ends with nothing live:

include textio
include fail

-- Holds a cell while it checks `x`; an error frees the cell by its consume.
fn checked(x: i32) -> i32 with Fail[i32] =
  let c = alloc(x * 10);
  if x < 0 { raise(x) };
  free(c)

main -> 1 =
  print_dec(with default(0) { checked(4) });
  print_byte(b' ');
  print_dec(with default(0) { checked(0 - 3) });
  print_byte(b'\n')
-- output: 40 0

A value that is not consumable (a capability, a one-shot closure that must be called, a must_close value) can’t be live where an error may leave, and the checker names it (What an abort does). An error’s payload is plain data, borrows or ! values.

The handler of Fail is a handler like any other, so it can be written for a purpose. to_result itself is

effect Fail[E] { ctl raise(e: E) -> never }

handler to_result[E, A]: Fail[E], A -> A + E = {
  return(a) = inl a;
  raise(e) = inr e
}

ctl raise(e: E) -> never declares an operation that never returns to its caller (it answers the empty type never), so the handler’s clause receives only the error.

Errors of several types

A function may raise errors of several types: Fail[i32] and Fail[ParseError] are two instances of Fail, each handled by the innermost handler of its own instance, whatever the order of the handlers. The library’s parsers raise their own error types: parse_i32 raises a ParseError (include text), which says why the text isn’t a number: inl () when it has no digits, inr (inl b) for a byte b that isn’t a digit, and inr (inr ()) for a number too large for an i32.

include textio

-- The two numbers of `"a b"`, added.
fn parse_sum<'t>(t: text 't) -> i32 with Fail[ParseError] =
  match split_once(t, b' ') {
    inl (a, b) -> parse_i32(a) + parse_i32(b)
  | inr t -> parse_i32(t)
  }

fn show(s: string) -> 1 =
  let r = borrow imm s { t -> with to_result { parse_sum(t) } };
  match r {
    inl n -> print_dec(n)
  | inr (inl u) -> print_lit("no digits")
  | inr (inr (inl b)) -> print_lit("not a digit: "); print_byte(b)
  | inr (inr (inr u)) -> print_lit("too large")
  };
  print_byte(b'\n')

main -> 1 =
  show(string_of_lit("12 30"));
  show(string_of_lit("12 x7"))
-- output: 42
-- output: not a digit: x

Several instances, and names shows two instances in one row, and two handlers of one instance told apart by name.

Division

a / b and a % b at i32, i64 and u8 truncate toward zero, the remainder taking the dividend’s sign.

Dividing by zero, or MIN / -1 at a signed width, raises Fail[DivError]. Its two values are div_by_zero and div_overflow. It’s an ordinary Fail, so to_result, default and map_err handle it, and a function can raise its own Fail[E] and divide too. Dividing by a nonzero literal can’t fail and needs no handler. checked_div and checked_rem return an Option-like sum instead. The Euclidean forms, div_euclid and rem_euclid, come with include arith (arith).

include textio

fn mean(sum: i32, n: i32) -> i32 = sum / n

main -> 1 =
  print_dec(with default(0) { mean(7, 0) });
  print_byte(b' ');
  match with to_result { mean(9, 3) } {
    inl m -> print_dec(m)
  | inr e -> print_lit("no mean")
  };
  print_byte(b' ');
  print_dec((0 - 7) / 2);
  print_byte(b'\n')
-- output: 0 3 -3

A division error that reaches main unhandled is not a compile-time error, so that arithmetic reads as it does elsewhere: the program unwinds, writes error: division by zero on standard error, and exits with status 1, after freeing what it held.

main -> i32 = let n = 0; 10 / n