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 and refusals

RCE has no exceptions. A step either finishes or is refused, and a function either returns a value or fails with a declared error.

Refusing an event

intent "a bank account that refuses overdrafts"
    example Deposit(amount: 50), Withdraw(amount: 20) -> 30
    example Withdraw(amount: 1) -> refused: Insufficient

state Account
    balance: i64 = 0

event Deposit(amount: i64)
event Withdraw(amount: i64)

error Insufficient(need: i64)
error NotPositive

struct Withdrawn
    amount: i64

step Account on Deposit(amount: a)
    if a <= 0
        fail NotPositive
    balance += a
step Account on Withdraw(amount: a)
    if a > balance
        fail Insufficient(need: a - balance)
    balance -= a
    emit Withdrawn(amount: a)
  • error Insufficient(need: i64) declares an error with a payload.
  • fail Insufficient(need: a - balance) refuses the event with that error.
  • emit Withdrawn(amount: a) produces an output value as part of the step.
  • event Deposit(amount: i64) has a payload, and the step's pattern Deposit(amount: a) names it a.
$ rce run account.rce "Deposit(amount: 50), Withdraw(amount: 20), Withdraw(amount: 40)"
0 (initial) Account { balance = 0 }
1 Deposit(amount: 50) Account { balance = 50 }
2 Withdraw(amount: 20) Account { balance = 30 }
  emit Withdrawn(amount: 20)
3 Withdraw(amount: 40) refused: Insufficient
exit status: 1

The third event asks for more than the balance, so it is refused and the balance stays at 30.

Functions that can fail

A function that can fail says so in its return type. u8 ! NotADigit means "a u8, or the error NotADigit".

error NotADigit(at: u32)

fn digit(c: text, at: u32) -> u8 ! NotADigit
    if c.isdigit()
        return c.toint() as u8
    fail NotADigit(at: at)

fn sum_digits(s: text) -> u32 ! NotADigit
    var total: u32 = 0
    var i: u32 = 0
    for c in s.chars()
        total += (try digit(c, i)) as u32
        i += 1
    return total

fn sum_or_zero(s: text) -> u32 = sum_digits(s) catch 0

fn where_it_broke(s: text) -> u32 = sum_digits(s) catch |e| e.at

check sum_or_zero("123") == 6
check sum_or_zero("1x3") == 0
check where_it_broke("12x") == 2
  • fail NotADigit(at: at) returns the error from the function.
  • try digit(c, i) uses the value if there is one, and otherwise passes the error on to the caller. That is why sum_digits also has ! NotADigit in its type.
  • sum_digits(s) catch 0 turns an error into a default value.
  • catch |e| e.at gives the error a name so you can use its payload.

A value of type u32 ! NotADigit is not a u32. The checker refuses code that uses it as one until the error is handled with try or catch, so an error cannot be ignored by accident. A step whose code fails with an error refuses the event.

Traps

A trap is a failure that is not a declared error: an integer that does not fit, a division by zero, a loop that reaches its limit. A trap inside a step refuses the event, exactly like fail. A trap in a check or an example makes the program inadmissible, as you saw in chapters 2 and 3.

What you learned

  • error, fail and refused events.
  • Error unions T ! E, with try, catch and catch |e|.
  • Traps refuse an event in the same way an error does.