Errors · Lesson 9.10

Error mapping

Source
Pass a failure to the caller with ? else, turning it into your own error and adding context such as a line number.
You'll need: Fail, Propagate

A low-level function knows what went wrong but not where. ParseNumber can say "the character at position 2 is not a digit", but it has no idea that its text came from line 3 of a settings file. The function that does know is the one calling it.

This lesson shows how that caller adds what it knows to the error as it passes through, with ? else (e => ...).

Plain ? cannot add context

Two error types meet here. ParseNumber fails with a DigitError, which knows a position; the settings reader fails with a SettingError, which needs a line and a column:

struct DigitError {
    position: uint;
}

struct SettingError {
    line: uint;
    column: uint;
}

Plain ? passes an error on unchanged, so it cannot add the line number — and it is rejected here anyway, because a DigitError is not a SettingError. The compiler's note is worth reading: '?' moves an error into the outer failure only by identity, sum member injection, or subset widening; it never converts an error.

? else maps the error

The mapped form fixes both problems:

let w = ParseNumber(width)? else (e => SettingError { line: 1, column: e.position + 1 });
let h = ParseNumber(height)? else (e => SettingError { line: 2, column: e.position + 1 });
PieceMeaning
ParseNumber(width)?on success, the number; the mapper never runs
else (e => ...)on failure, e is the whole original DigitError
SettingError { ... }the new error, built from e and anything else in scope; the function fails with it
flowchart LR
    call["ParseNumber(width)"] --> q{"Success or failure?"}
    q -- "success" --> n["the number;<br/>the mapper never runs"]
    q -- "failure, as e" --> m["the mapper builds<br/>SettingError { line: 1, column: e.position + 1 }"]
    m --> f["WindowArea fails<br/>with the new error"]

The mapper uses what is in scope

The mapper is a single expression, and it can read anything the function can. Here it adds the line number, which only WindowArea knows, and turns the 0-based position into the 1-based column a person would look for. In "8O" the letter O sits at position 1, so the message says column 2.

The parentheses around e => ... hold exactly one name and one expression. The mapper must produce the enclosing function's error type — the compiler checks that, but not whether the arithmetic inside is right.

Where to map

Map an error where the context is. ParseNumber stays small and reusable because it knows nothing about files; WindowArea is the first function that knows about lines, so it is the one that maps. Its own caller then deals with SettingError alone and never needs to know that a DigitError existed.

The program

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

Src/Main.rux
// A low-level function knows what went wrong but not where. `ParseNumber` below
// can say "the character at position 2 is not a digit", but it has no idea that
// its text came from line 3 of a settings file. The function that *does* know
// is the one calling it.
//
// Plain `?` passes an error on unchanged, so it cannot add that context — and
// it is rejected here anyway, because a `DigitError` is not a `SettingError`.
// The mapped form fixes both:
//
//     ParseNumber(text)? else (e => SettingError { ... })
//
// On success the expression is the number and the mapper never runs. On
// failure `e` is the whole original error, the mapper builds the new one from
// `e` plus anything else in scope, and the function fails with it.
import Io::PrintLine;

struct DigitError {
    position: uint;
}

struct SettingError {
    line: uint;
    column: uint;
}

// Knows about characters, nothing about files or lines.
func ParseNumber(text: char8[..]) -> int ! DigitError {
    var total = 0;
    for i in 0..text.length {
        let byte = text[i];
        if byte < c8'0' || byte > c8'9' {
            fail DigitError { position: i };
        }
        total = total * 10 + ((byte as int) - 48);
    }
    return total;
}

// Reads a window size from two settings lines. Each `? else` adds the line
// number, which only this function knows, and turns the 0-based position into
// the 1-based column a person would look for.
func WindowArea(width: char8[..], height: char8[..]) -> int ! SettingError {
    let w = ParseNumber(width)? else (e => SettingError { line: 1, column: e.position + 1 });
    let h = ParseNumber(height)? else (e => SettingError { line: 2, column: e.position + 1 });
    return w * h;
}

func Show(width: char8[..], height: char8[..]) {
    match WindowArea(width, height) {
        .Success(area) => PrintLine("{} x {}: area {}", width, height, area),
        .Failure(e) => PrintLine("{} x {}: line {}, column {} is not a digit",
            width, height, e.line, e.column)
    }
}

func Main() -> int {
    Show("80", "24");
    Show("8O", "24");
    Show("80", "2-4");
    return 0;
}

Run it

cd Examples/Errors/ErrorMapping
rux run
80 x 24: area 1920
8O x 24: line 1, column 2 is not a digit
80 x 2-4: line 2, column 2 is not a digit

Common mistakes

Plain ? across error types.
let w = ParseNumber(width)?; is error: '?' propagates error type 'DigitError', but the enclosing function fails with 'SettingError', with the help map the error to 'SettingError' with '? else (e => ...)', or match the value.
A mapper that builds the wrong type.
ParseNumber(width)? else (e => 5) is error: the mapped error has type 'int', but the enclosing function fails with 'SettingError'.
Off by one in the mapper.
Write column: e.position and the program still compiles — it just reports column 1 for "8O". The compiler checks the mapper's type, not its meaning.

Try it yourself

  1. Add a found: char8 field to SettingError and fill it in the mapper with the bad character, width[e.position]. Print it in Show.
  2. Call Show("", "24"). Predict the output before you run it — is an empty line an error?
  3. Read a third setting, a depth on line 3, and print the volume instead of the area.

Learn more

  • Propagate — plain ?, which passes an error on unchanged
  • Error variant — a richer target type for a mapper
  • Error sum — passing on two error types without mapping either