Errors · Lesson 9.8

Propagate

Source
Hand a failure to the caller with postfix ?, which keeps the success and passes the error on unchanged.
You'll need: Outcome, Catch

Handling a fallible with match or catch is right when this function is the one that should decide what a failure means. Often it is not. The function is a step in the middle, and a failure should simply become the caller's problem.

Postfix ? does exactly that. On success, step? is the value inside. On failure, the enclosing function fails at once with that same error, and nothing after the ? runs.

The long way

Written by hand, passing a failure on is the same few lines every time: match, keep the success, fail with the error unchanged.

var high = 0;
match ReadDigit(tens) {
    .Success(value) => high = value,
    .Failure(error) => fail error
}

TwoDigitsByHand writes that block twice, once per digit. The two blocks are near-identical and say nothing new.

The same with ?

? is those lines written once. The success values are used straight away:

func TwoDigits(tens: char, units: char) -> int ! DigitError {
    let high = ReadDigit(tens)?;
    let low = ReadDigit(units)?;
    return high * 10 + low;
}
flowchart LR
    step["ReadDigit(tens)?"] --> q{"Success or failure?"}
    q -- ".Success(value)" --> go["the expression is value;<br/>TwoDigits carries on"]
    q -- ".Failure(error)" --> out["TwoDigits fails at once<br/>with the same error"]
    out --> caller["the caller's match<br/>sees the original error"]

Both versions behave identically: ? is shorthand, not a different rule. When both digits are wrong, as in TwoDigits('x', '?'), only the first one is reported — the first ? leaves, and the second read never happens.

The error travels intact

Neither TwoDigits nor ? looked at the error, yet the caller still learns which character was wrong:

.Failure(DigitError::NotADigit(c)) => PrintLine("'{}' is not a digit", c)

Show matches the nested pattern .Failure(DigitError::NotADigit(c)) directly, reaching through the failure channel into the variant case in one step.

Two requirements

? asks two things of the function it is used in:

  1. It must be fallible, since that is where the failure goes. Main returns a plain int, so ReadDigit('4')? there is rejected.
  2. The error must fit its error type. Here both are DigitError, so it passes through as is. A different error type needs Error mapping, or an error sum that includes it.
ToolWho decides what a failure meansThe function stays
match, catchthis functionas fallible as you choose
?the callerfallible, with the same error type

The program

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

Src/Main.rux
// Handling a fallible with `match` or `catch` is right when this function is the one that should
// decide what a failure means. Often it is not: the function is a step in the middle, and a
// failure should simply become the caller's problem.
//
// Written by hand that is the same few lines every time: match, keep the success, fail with the
// error unchanged. Postfix `?` is those lines written once. On success, `step?` is the value
// inside. On failure, the enclosing function fails at once with that same error, and nothing after
// the `?` runs.
import Io::PrintLine;

variant DigitError {
    Blank,
    NotADigit(char)
}

// One small step that can fail.
func ReadDigit(c: char) -> int ! DigitError {
    if c == ' ' {
        fail DigitError::Blank;
    }
    if c < '0' || c > '9' {
        fail DigitError::NotADigit(c);
    }
    return (c as int) - ('0' as int);
}

// The long way. Each failure is matched only to be handed straight back to the caller: two
// near-identical blocks that say nothing new.
func TwoDigitsByHand(tens: char, units: char) -> int ! DigitError {
    var high = 0;
    match ReadDigit(tens) {
        .Success(value) => high = value,
        .Failure(error) => fail error
    }
    var low = 0;
    match ReadDigit(units) {
        .Success(value) => low = value,
        .Failure(error) => fail error
    }
    return high * 10 + low;
}

// The same function with `?`. The success values are used straight away.
func TwoDigits(tens: char, units: char) -> int ! DigitError {
    let high = ReadDigit(tens)?;
    let low = ReadDigit(units)?;
    return high * 10 + low;
}

func Show(outcome: int ! DigitError) {
    match outcome {
        .Success(value) => PrintLine("read {}", value),
        .Failure(DigitError::Blank) => PrintLine("a digit is missing"),
        .Failure(DigitError::NotADigit(c)) => PrintLine("'{}' is not a digit", c)
    }
}

func Main() -> int {
    Show(TwoDigits('4', '2'));
    Show(TwoDigits('4', 'x'));
    Show(TwoDigits('?', '2'));
    Show(TwoDigits(' ', '7'));

    // Both versions behave identically: `?` is shorthand, not a different rule.
    Show(TwoDigitsByHand('4', 'x'));

    // The error travels intact. Neither `TwoDigits` nor `?` looked at it, yet the caller still
    // learns which character was wrong.
    //
    // `?` has two requirements. The function using it must itself be fallible, since that is
    // where the failure goes. In `Main`, which returns a plain `int`, `ReadDigit('4')?` is
    // rejected: "'?' propagates native fallible 'int ! DigitError', but the enclosing function
    // returns 'int'". And the error must fit that function's error type. Here both are
    // `DigitError`, so it passes through as is.
    return 0;
}

Run it

cd Examples/Errors/Propagate
rux run
read 42
'x' is not a digit
'?' is not a digit
a digit is missing
'x' is not a digit

Common mistakes

? in a function that cannot fail.
In Main, ReadDigit('4')? is error: '?' propagates native fallible 'int ! DigitError', but the enclosing function returns 'int'. Handle the failure with match or catch there — or make Main fallible, as Fallible main shows.
Forgetting the ?.
let high = ReadDigit(tens); makes high the whole fallible, and the next line fails: error: operator '*' cannot combine left operand 'int ! DigitError' with right operand 'int'.
Passing on an error of another type.
? never converts an error. In a function that fails with a SettingError, ? on a DigitError is error: '?' propagates error type 'DigitError', but the enclosing function fails with 'SettingError'. Error mapping is the fix.

Try it yourself

  1. Add Show(TwoDigits(' ', 'x')); and predict which of the two messages it prints.
  2. Write ThreeDigits(hundreds: char, tens: char, units: char) -> int ! DigitError with three ?.
  3. Rewrite ThreeDigits to call TwoDigits for the last two digits. Does the ? still need anything special?

Learn more