Errors · Lesson 9.1

Fallible

Source
Return a value or an error with T ! E, where return value; is the success, and tell the two apart with .Success and .Failure.
You'll need: Optional, Variant

An optional says that a value is missing, but never why. Often the why is exactly what the caller needs. Dividing 12 by 0 and dividing 13 by 4 both fail to give an exact answer — for different reasons that deserve different replies.

A fallible type carries the reason with it. int ! DivideError reads "an int, or else a DivideError". This lesson introduces the type and shows how a caller finds out which of the two it got; the rest of Part 9 is about the many ways of handling the second one.

Two channels

A fallible has two channels. The success channel holds the answer. The failure channel holds an error — an ordinary value describing what went wrong. The error type is whatever suits, and a variant is a natural fit: one case for each way the operation can go wrong.

variant DivideError {
    ByZero,
    NotExact(int)
}

NotExact carries the remainder with it, so the caller can say how far off the division was. The signature puts the two channels side by side, success type first, error type after the !:

func ExactDivide(numerator: int, denominator: int) -> int ! DivideError {
TypeHolds
intalways an int
int?an int, or nothing — with no reason given
int ! DivideErroran int, or a DivideError saying what went wrong

The success needs no ceremony

Inside a fallible function, return value; is the success, just as a plain value returned from an int? function is a present one. fail, the subject of the next lesson, leaves through the failure channel instead:

if denominator == 0 {
    fail DivideError::ByZero;
}
if numerator % denominator != 0 {
    fail DivideError::NotExact(numerator % denominator);
}
return numerator / denominator;
flowchart LR
    call["ExactDivide(12, 0)"] --> body{"Which way does<br/>the function leave?"}
    body -- "return value;" --> s[".Success(value)<br/>the success channel"]
    body -- "fail error;" --> f[".Failure(error)<br/>the failure channel"]
    s --> caller["The caller opens it:<br/>match, catch or ?"]
    f --> caller

Opening the result

What comes back is not yet an int. To use the answer, find out which channel it came through. .Success(value) matches the answer and binds it; .Failure(error) matches an error and binds that:

match ExactDivide(numerator, denominator) {
    .Success(value) => PrintLine("{} / {} = {}", numerator, denominator, value),
    .Failure(error) => Explain(numerator, denominator, error)
}

The error is an ordinary DivideError, so Explain uses a second match to pick out its case — the same variant match you already know:

match error {
    DivideError::ByZero => PrintLine("{} / {} fails: no dividing by zero", numerator,
        denominator),
    DivideError::NotExact(remainder) => PrintLine("{} / {} fails: {} left over", numerator,
        denominator, remainder)
}

The match must cover both channels. A failure can never slip past unnoticed: the compiler refuses a match that forgets .Failure, and — as you will see in Discard — a call whose result nobody looks at.

The program

The whole lesson is one package in the Examples repository. Its comments explain every step.

Src/Main.rux
// An optional says that a value is missing, but never why. Often the why is exactly what the
// caller needs: dividing 12 by 0 and dividing 13 by 4 both fail to give an exact answer, for
// different reasons that deserve different replies.
//
// `int ! DivideError` reads "an `int`, or else a `DivideError`". A fallible like this has two
// channels. The success channel holds the answer. The failure channel holds an error, a value
// describing what went wrong. The error type is whatever suits, and a variant is a natural fit:
// one case for each way the operation can go wrong.
//
// The success needs no ceremony. `return value;` in a fallible function is the success, just as a
// plain value returned from an `int?` function is a present one.
import Io::PrintLine;

// The ways an exact division can go wrong. `NotExact` carries the remainder with it.
variant DivideError {
    ByZero,
    NotExact(int)
}

// The signature says it all: this produces an `int`, or a `DivideError`. `fail`, the subject of
// the next lesson, leaves through the failure channel; an ordinary `return` is the success.
func ExactDivide(numerator: int, denominator: int) -> int ! DivideError {
    if denominator == 0 {
        fail DivideError::ByZero;
    }
    if numerator % denominator != 0 {
        fail DivideError::NotExact(numerator % denominator);
    }
    return numerator / denominator;
}

// To use the answer, find out which channel it came through. `.Success(value)` matches the answer
// and binds it; `.Failure(error)` matches an error and binds that. The error is an ordinary
// `DivideError`, so a second match picks out its case.
func Report(numerator: int, denominator: int) {
    match ExactDivide(numerator, denominator) {
        .Success(value) => PrintLine("{} / {} = {}", numerator, denominator, value),
        .Failure(error) => Explain(numerator, denominator, error)
    }
}

func Explain(numerator: int, denominator: int, error: DivideError) {
    match error {
        DivideError::ByZero => PrintLine("{} / {} fails: no dividing by zero", numerator,
            denominator),
        DivideError::NotExact(remainder) => PrintLine("{} / {} fails: {} left over", numerator,
            denominator, remainder)
    }
}

func Main() -> int {
    Report(12, 4);
    Report(12, 0);
    Report(13, 4);

    // A fallible is not an `int` until it has been opened. `let half = ExactDivide(12, 2) / 2;`
    // is rejected, and so is calling `ExactDivide(12, 4);` and ignoring what comes back: a
    // failure can never be dropped without the code saying so.
    return 0;
}

Run it

cd Examples/Errors/Fallible
rux run
12 / 4 = 3
12 / 0 fails: no dividing by zero
13 / 4 fails: 1 left over

Common mistakes

Using a fallible as if it were the answer.
let half = ExactDivide(12, 2) / 2; fails with error: operator '/' cannot combine left operand 'int ! DivideError' with right operand 'int'. The int is inside the success channel and has to be taken out first — with match here, and later with catch or ?.
Calling it and ignoring what comes back.
ExactDivide(12, 4); on its own is error: fallible result of type 'int ! DivideError' is discarded, with the note a failure that nothing handles is lost. Discard shows how to ignore a result on purpose.
Returning the error.
return DivideError::ByZero; is refused with error: 'return' value must have type 'int ! DivideError', but found 'DivideError'. return is the success channel; an error leaves with fail.

Try it yourself

  1. Add Report(0, 5); to Main. Predict the line it prints before you run it.
  2. Delete the .Failure arm from Report's match and read the error. Which pattern does the compiler say is missing?
  3. Add a case Negative to DivideError and fail with it when the denominator is below zero. Where does the compiler send you next, and why?

Learn more

  • Fail — the failure channel's return
  • Outcome — storing, passing and building a fallible's result
  • Optional — the simpler form, for a value that may be missing with no reason given
  • Error handling in the Rux Reference