Handling Failures

A function that receives a fallible T ! E either handles the failure itself or passes it on. This page covers handling it: a match over both channels, postfix catch, and deliberately discarding a result. Passing it on is Error propagation.

Matching

An ordinary match selects a channel with the patterns .Success(p) and .Failure(p). The inner pattern can be anything that matches the payload — a binding, a literal, a variant case, a typed pattern for an error sum, or another channel pattern for a nested fallible:

func Percent(text: char8[..]) -> int {
    return match ParsePercent(text) {
        .Success(value) => value,
        .Failure(_: DigitError) => -1,
        .Failure(error: RangeError) => -error.value
    };
}
PatternMatches
.Success(p)a success whose value matches p
.Success(())the success of a ! E
.Failure(p)a failure whose error matches p
.Failure(e: A)a failure whose error is the member A of an error sum
.Failure(E::Case(x))a failure holding one case of a variant error
.Success(.Failure(e))an inner failure held as the outer success

Coverage is checked across both channels and every level, and the diagnostic names what is missing:

error: match on 'int ! NobodyToShare' is not exhaustive; missing .Failure(_)
error: match on 'int ! (DigitError | RangeError)' is not exhaustive; missing .Failure(_: RangeError)
error: match on 'int? ! SensorError' is not exhaustive; missing .Success(none)

A .Success pattern always has one field. For a ! E it is the unit: .Success => … is error: pattern '.Success' expects 1 field, but found 0; write .Success(()) or .Success(_).

catch

catch recovers from the failure of one fallible. It is postfix, written right after the fallible expression, and its arms match the error only:

catch-expr = postfix-expr "catch" "{" match-arm { "," match-arm } [ "," ] "}"
variant DigitError {
    Blank,
    Letter(char)
}

func Lenient(c: char) -> int {
    return ReadDigit(c) catch {
        DigitError::Blank => 0,
        DigitError::Letter('O') => 0,
        DigitError::Letter(_) => -1
    };
}
  • The subject is evaluated once. A success passes through unchanged and no arm runs; the expression's value is the success value.
  • A failure is matched against the arms, which use ordinary match-arm grammar — guards and an else arm included — and must cover every error value: error: match on 'DigitError' is not exhaustive; missing DigitError::Blank.
  • Each arm either produces a value of the success type or leaves the function. DigitError::Blank => "blank" in the example is error: 'catch' arm produces 'char8[..]', but the recovered value has type 'int'.
  • The result of catch is a plain T — the fallible is fully handled.

An arm leaves with fail, return, break, continue, a call to Panic or a #NoReturn() function. A leaving arm does not decide the result type, and its fail or ? is an ordinary exit from the enclosing function — the same catch never sees it again:

func Strict(c: char) -> int ! IoError {
    let digit = ReadDigit(c) catch { e => fail IoError { code: 22 } };
    return digit * 2;
}

let trusted = ReadDigit('8') catch { e => Panic("a literal digit always reads") };

catch applies only to a native fallible. On an optional or a plain value it is error: 'catch' recovers a native fallible, but the subject has type 'int?'; the optional counterpart is ??.

A fallback for every failure

catch { else => fallback } is the fallible counterpart of ??: the success value, or fallback for any failure.

let digit = ReadDigit(c) catch { else => 0 };

Unlike ??, catch has no brace-less form; the braces are always written.

Block arms

An arm whose body is a block { … } completes with (). It is therefore valid only where the success type is () — a ! E — unless the block ends by leaving:

Close(false) catch {
    e => {
        PrintLine("close failed with {}", e.code);
    }
};

On a fallible with a value, an empty block is rejected rather than inventing one:

error: a block arm completes with '()', but 'catch' must recover a value of type 'int32'
  help: give the arm a value, or leave it with 'fail', 'return', or a call to 'Panic'

An expression arm has to have the success type too. e => PrintLine("…") in a ! E catch is an error, because PrintLine returns IoError?, not (); put the call in a block.

Binding

catch binds tighter than every binary operator, to the postfix expression right before it, and a postfix chain continues after the closing brace:

PrintLine("{}", 1 + ReadDigit('x') catch { else => 10 });  // 11: only ReadDigit is recovered
PrintLine("{}", ReadDigit('4') catch { else => 0 } * 10);  // 40

A match expression may be the subject: let picked = match key { … } catch { else => -1 };. A match statement takes no postfix operator.

catch handles one level. On a (int32 ! ParseError) ! IoError, only the outer IoError reaches the arms; an inner failure is part of the success and passes through.

catch consumes its subject. A named copyable fallible is copied; a named move-only one is written (<-outcome) catch { … }. An arm that binds the error owns it, and an error no arm binds is destroyed when its arm is chosen.

Discarding a result

A failure that nothing looks at is lost, so the compiler rejects the ways a fallible can be silently thrown away:

CodeResult
Save(true); — an expression statementerror
let _ = Save(true);error
a match statement arm whose bare expression is a fallibleerror
let r = Save(true); and r is never readwarning
Save(true) catch { else => {} };a deliberate discard — only for a ! E
a match with .Success(_) => {} and .Failure(_) => {}a deliberate discard of any fallible
error: fallible result of type '! IoError' is discarded
  note: a failure that nothing handles is lost
  help: propagate it with '?', recover with 'catch', or match both '.Success' and '.Failure'

For let _, the note reads binding a fallible to '_' does not handle its failure. The unread local is warning: fallible local 'r' is never read; its failure is never handled.

Deliberate discards, for cleanup whose outcome does not matter:

Close(false) catch { else => {} };   // a ! E

match ReadDigit('5') {               // a fallible with a value
    .Success(_) => {},
    .Failure(_) => {}
}

These checks are practical, not a proof that every error is handled. Reading, passing, returning, storing or overwriting a fallible counts as a use, and nothing is followed further — through fields, containers or later writes.

PrintLine is not fallible.
Io::PrintLine returns IoError? — an optional that reports a console write problem. An optional may be ignored, which is why PrintLine(…); is a valid statement while a bare call to a fallible function is not.

Asking without handling

Core provides two generic functions for code that needs only the answer, such as a branch or an assertion:

import Core::{ Failed, Succeeded };
FunctionReturns
Succeeded<T, E>(outcome: T ! E) -> boolwhether outcome holds a success
Failed<T, E>(outcome: T ! E) -> boolwhether outcome holds a failure

Both consume their argument; the payload is destroyed after the question is answered. is does not test channels — outcome is int32 on a fallible is error: 'is' cannot test the channel of fallible 'int32 ! IoError'.

Idioms

WantedWrite
a fallback value for any failureoutcome catch { else => fallback }
a fallback per kind of failureoutcome catch { Kind::A => …, Kind::B => … }
an expected success, where failure is a bugoutcome catch { e => Panic("why it cannot fail") }
translate the failure and keep failingoutcome catch { e => fail Wrap(e) }, or ? else
pass the failure on unchangedoutcome?
ignore a unit fallibleoutcome catch { else => {} };

?? is not one of them: ReadDigit('7') ?? 5 is error: operator '??' cannot take 'int ! DigitError', because coalescing would discard the error.

See also