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
};
}
| Pattern | Matches |
|---|---|
.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
elsearm 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 iserror: 'catch' arm produces 'char8[..]', but the recovered value has type 'int'. - The result of
catchis a plainT— 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:
| Code | Result |
|---|---|
Save(true); — an expression statement | error |
let _ = Save(true); | error |
| a match statement arm whose bare expression is a fallible | error |
let r = Save(true); and r is never read | warning |
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.
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 };
| Function | Returns |
|---|---|
Succeeded<T, E>(outcome: T ! E) -> bool | whether outcome holds a success |
Failed<T, E>(outcome: T ! E) -> bool | whether 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
| Wanted | Write |
|---|---|
| a fallback value for any failure | outcome catch { else => fallback } |
| a fallback per kind of failure | outcome catch { Kind::A => …, Kind::B => … } |
| an expected success, where failure is a bug | outcome catch { e => Panic("why it cannot fail") } |
| translate the failure and keep failing | outcome catch { e => fail Wrap(e) }, or ? else |
| pass the failure on unchanged | outcome? |
| ignore a unit fallible | outcome 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
- Errors — the fallible type and how failures are produced
- Error propagation — handing a failure to the caller
- Match and Patterns
- Learn: Outcome, Discard, Catch, Catch fallback