Errors · Lesson 9.13

Absence to error

Source
Turn a missing value into a failure that says what was missing, with option ?? fail.
You'll need: Coalesce, Fail

An optional says "nothing here" and stops there. PriceOf(7) returns none, and none cannot say which code was missing or why that matters. When absence is a real problem for the caller, turn it into an error that does say so.

?? fail

A small price list where an unknown code is simply absent:

func PriceOf(code: int) -> int? {
    return match code {
        1 => 250,
        2 => 120,
        3 => 75,
        else => none
    };
}

Total needs a price, and a missing one is a failure worth reporting:

func Total(code: int, quantity: int) -> int ! UnknownProduct {
    let price = PriceOf(code) ?? fail UnknownProduct { code: code };
    return price * quantity;
}

This is the ?? from Coalesce, with fail as its fallback — just as Coalesce exit used return. When the price is present, price is that number. When it is absent, the function fails right there, and the error can carry whatever context is at hand — here, the code that was not found.

flowchart LR
    look["PriceOf(code)"] --> q{"Present or none?"}
    q -- "present" --> price["price is the number;<br/>Total carries on"]
    q -- "none" --> fail["fail UnknownProduct { code }"]
    fail --> caller["the caller learns<br/>which code was missing"]

The error is built only when needed

Like every ?? fallback, the right side runs only when it is needed. For a product that exists, the UnknownProduct value is never even built.

Why not ?

On an optional, ? passes the absence on — but only as absence. It works in a function that returns an optional, and Total returns a fallible. ? never invents an error for a missing value; ?? fail is how you supply one yourself.

You writeWhen the price is missing, Total…
PriceOf(code) ?? 0carries on with 0, so the order costs nothing
PriceOf(code) ?? return …succeeds at once with the value given to return
PriceOf(code)?is rejected: Total returns a fallible, not an optional
PriceOf(code) ?? fail …fails, saying which code it was

The program

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

Src/Main.rux
// An optional says "nothing here" and stops there. `PriceOf(7)` returns
// `none`, and `none` cannot say which code was missing or why that matters.
// When absence is a real problem for the caller, turn it into an error that
// does say so:
//
//     let price = PriceOf(code) ?? fail UnknownProduct { code: code };
//
// This is the `??` from the optionals part with `fail` as its fallback. When
// the price is present, `price` is that number. When it is absent, the
// function fails right there, and the error can carry whatever context is at
// hand — here, the code that was not found.
//
// Like every `??` fallback, the right side runs only when it is needed, so
// the error is not even built for a product that exists.
import Io::PrintLine;

struct UnknownProduct {
    code: int;
}

// A small price list. An unknown code is simply absent.
func PriceOf(code: int) -> int? {
    return match code {
        1 => 250,
        2 => 120,
        3 => 75,
        else => none
    };
}

func Total(code: int, quantity: int) -> int ! UnknownProduct {
    let price = PriceOf(code) ?? fail UnknownProduct { code: code };
    return price * quantity;
}

func Order(code: int, quantity: int) {
    match Total(code, quantity) {
        .Success(total) => PrintLine("{} x product {}: {} cents", quantity, code, total),
        .Failure(e) => PrintLine("{} x product {}: no product has code {}", quantity, code, e.code)
    }
}

func Main() -> int {
    Order(1, 2);
    Order(3, 4);
    Order(7, 1);
    return 0;
}

Run it

cd Examples/Errors/AbsenceToError
rux run
2 x product 1: 500 cents
4 x product 3: 300 cents
1 x product 7: no product has code 7

Common mistakes

Using ? to turn absence into an error.
let price = PriceOf(code)?; in Total is error: '?' propagates the absence of 'int?', but the enclosing function returns 'int ! UnknownProduct', with the note '?' never invents an error for it. Write ?? fail with the error you mean.
?? fail in a function that cannot fail.
In Main, PriceOf(1) ?? fail UnknownProduct { code: 1 } is error: 'fail' needs an enclosing fallible function, but this function returns 'int'.
Comparing without parentheses.
== binds tighter than ??, so PriceOf(1) ?? 0 == 250 means PriceOf(1) ?? (0 == 250) and fails with error: coalescing fallback has type 'bool8', but the optional payload is 'int'. Write (PriceOf(1) ?? 0) == 250.

Try it yourself

  1. Add product 4 at 990 cents and order three of them.
  2. Refuse an order of zero items as well. One error struct no longer fits — try a variant with an UnknownCode(int) case and a NoItems case.
  3. Change Order(7, 1) to use PriceOf(7) ?? 0 directly in Main instead of Total. What does the customer pay for an unknown product?

Learn more