Fallible
An optional says that a value is missing, but never why. Often the why is exactly what the caller needs. Dividing 12 by 0 and dividing 13 by 4 both fail to give an exact answer — for different reasons that deserve different replies.
A fallible type carries the reason with it. int ! DivideError reads "an int, or else a DivideError". This lesson introduces the type and shows how a caller finds out which of the two it got; the rest of Part 9 is about the many ways of handling the second one.
Two channels
A fallible has two channels. The success channel holds the answer. The failure channel holds an error — an ordinary value describing what went wrong. The error type is whatever suits, and a variant is a natural fit: one case for each way the operation can go wrong.
variant DivideError {
ByZero,
NotExact(int)
}
NotExact carries the remainder with it, so the caller can say how far off the division was. The signature puts the two channels side by side, success type first, error type after the !:
func ExactDivide(numerator: int, denominator: int) -> int ! DivideError {
| Type | Holds |
|---|---|
int | always an int |
int? | an int, or nothing — with no reason given |
int ! DivideError | an int, or a DivideError saying what went wrong |
The success needs no ceremony
Inside a fallible function, return value; is the success, just as a plain value returned from an int? function is a present one. fail, the subject of the next lesson, leaves through the failure channel instead:
if denominator == 0 {
fail DivideError::ByZero;
}
if numerator % denominator != 0 {
fail DivideError::NotExact(numerator % denominator);
}
return numerator / denominator;
flowchart LR
call["ExactDivide(12, 0)"] --> body{"Which way does<br/>the function leave?"}
body -- "return value;" --> s[".Success(value)<br/>the success channel"]
body -- "fail error;" --> f[".Failure(error)<br/>the failure channel"]
s --> caller["The caller opens it:<br/>match, catch or ?"]
f --> callerOpening the result
What comes back is not yet an int. To use the answer, find out which channel it came through. .Success(value) matches the answer and binds it; .Failure(error) matches an error and binds that:
match ExactDivide(numerator, denominator) {
.Success(value) => PrintLine("{} / {} = {}", numerator, denominator, value),
.Failure(error) => Explain(numerator, denominator, error)
}
The error is an ordinary DivideError, so Explain uses a second match to pick out its case — the same variant match you already know:
match error {
DivideError::ByZero => PrintLine("{} / {} fails: no dividing by zero", numerator,
denominator),
DivideError::NotExact(remainder) => PrintLine("{} / {} fails: {} left over", numerator,
denominator, remainder)
}
The match must cover both channels. A failure can never slip past unnoticed: the compiler refuses a match that forgets .Failure, and — as you will see in Discard — a call whose result nobody looks at.
The program
The whole lesson is one package in the Examples repository. Its comments explain every step.
// An optional says that a value is missing, but never why. Often the why is exactly what the
// caller needs: dividing 12 by 0 and dividing 13 by 4 both fail to give an exact answer, for
// different reasons that deserve different replies.
//
// `int ! DivideError` reads "an `int`, or else a `DivideError`". A fallible like this has two
// channels. The success channel holds the answer. The failure channel holds an error, a value
// describing what went wrong. The error type is whatever suits, and a variant is a natural fit:
// one case for each way the operation can go wrong.
//
// The success needs no ceremony. `return value;` in a fallible function is the success, just as a
// plain value returned from an `int?` function is a present one.
import Io::PrintLine;
// The ways an exact division can go wrong. `NotExact` carries the remainder with it.
variant DivideError {
ByZero,
NotExact(int)
}
// The signature says it all: this produces an `int`, or a `DivideError`. `fail`, the subject of
// the next lesson, leaves through the failure channel; an ordinary `return` is the success.
func ExactDivide(numerator: int, denominator: int) -> int ! DivideError {
if denominator == 0 {
fail DivideError::ByZero;
}
if numerator % denominator != 0 {
fail DivideError::NotExact(numerator % denominator);
}
return numerator / denominator;
}
// To use the answer, find out which channel it came through. `.Success(value)` matches the answer
// and binds it; `.Failure(error)` matches an error and binds that. The error is an ordinary
// `DivideError`, so a second match picks out its case.
func Report(numerator: int, denominator: int) {
match ExactDivide(numerator, denominator) {
.Success(value) => PrintLine("{} / {} = {}", numerator, denominator, value),
.Failure(error) => Explain(numerator, denominator, error)
}
}
func Explain(numerator: int, denominator: int, error: DivideError) {
match error {
DivideError::ByZero => PrintLine("{} / {} fails: no dividing by zero", numerator,
denominator),
DivideError::NotExact(remainder) => PrintLine("{} / {} fails: {} left over", numerator,
denominator, remainder)
}
}
func Main() -> int {
Report(12, 4);
Report(12, 0);
Report(13, 4);
// A fallible is not an `int` until it has been opened. `let half = ExactDivide(12, 2) / 2;`
// is rejected, and so is calling `ExactDivide(12, 4);` and ignoring what comes back: a
// failure can never be dropped without the code saying so.
return 0;
}
Run it
cd Examples/Errors/Fallible
rux run
12 / 4 = 3
12 / 0 fails: no dividing by zero
13 / 4 fails: 1 left over
Common mistakes
let half = ExactDivide(12, 2) / 2; fails with error: operator '/' cannot combine left operand 'int ! DivideError' with right operand 'int'. The int is inside the success channel and has to be taken out first — with match here, and later with catch or ?.ExactDivide(12, 4); on its own is error: fallible result of type 'int ! DivideError' is discarded, with the note a failure that nothing handles is lost. Discard shows how to ignore a result on purpose.return DivideError::ByZero; is refused with error: 'return' value must have type 'int ! DivideError', but found 'DivideError'. return is the success channel; an error leaves with fail.Try it yourself
- Add
Report(0, 5);toMain. Predict the line it prints before you run it. - Delete the
.Failurearm fromReport's match and read the error. Which pattern does the compiler say is missing? - Add a case
NegativetoDivideErrorand fail with it when the denominator is below zero. Where does the compiler send you next, and why?
Learn more
- Fail — the failure channel's
return - Outcome — storing, passing and building a fallible's result
- Optional — the simpler form, for a value that may be missing with no reason given
- Error handling in the Rux Reference
Overview
Operations that can fail: T ! E. Sixteen lessons on reporting a failure with fail, handling it with match and catch, passing it on with ?, designing error types, and stopping the program with Panic and Assert when only a bug could be to blame.
9.2 Fail
Leave a fallible function through its failure channel with fail, carrying an error that says what went wrong.