Fail
A fallible function has two ways out. return value; is the success, as the previous lesson showed. fail is the other one: it leaves the function at once through the failure channel, carrying an error value for the caller.
This lesson is a cash machine. A withdrawal either answers with the new balance or refuses — and when it refuses, it says exactly how much money was missing.
The error is plain data
An error can be any type you like. Here it is a struct, so it can report exactly what went wrong — how much the account held and how much was asked for:
struct ShortOfFunds {
balance: int;
requested: int;
}
There is nothing special about the type itself. It becomes an error only because a signature names it after the !:
func Withdraw(balance: int, amount: int) -> int ! ShortOfFunds {
fail leaves at once
fail builds the error and hands it over in one statement. Nothing after a fail runs, just as nothing after a return does:
if amount > balance {
fail ShortOfFunds { balance: balance, requested: amount };
}
// Only reached when the money is there.
return balance - amount;
| Statement | Leaves through | The caller sees |
|---|---|---|
return balance - amount; | the success channel | .Success(left) |
fail ShortOfFunds { ... }; | the failure channel | .Failure(error) |
Reading the details
Because the error is a struct, the caller reads its fields like any other. The refusal message works out the shortfall from the two numbers the error carried:
.Failure(error) => PrintLine("take {} from {}: refused, {} short", amount, balance,
error.requested - error.balance)
Taking exactly the whole balance is fine — amount > balance is false for 100 and 100, so Report(100, 100) leaves 0. An empty account refuses even 5.
Where fail is allowed
fail has two rules, and the compiler enforces both. Its value must have the error type named after the ! — a ShortOfFunds here, not a number. And it belongs only in a function that can fail: Main returns a plain int, so a fail there has nowhere to go.
The program
The whole lesson is one package in the Examples repository. Its comments explain every step.
// A fallible function has two ways out. `return value;` is the success, as the previous lesson
// showed. `fail` is the other one: it leaves the function at once through the failure channel,
// carrying an error value for the caller.
//
// The error can be any type you like. Here it is a struct, so it can report exactly what went
// wrong — how much money the account held and how much was asked for. `fail ShortOfFunds { ... };`
// builds that struct and hands it over in one statement. Nothing after a `fail` runs, just as
// nothing after a `return` does.
import Io::PrintLine;
// The error: plain data describing the problem, nothing special about the type itself.
struct ShortOfFunds {
balance: int;
requested: int;
}
// Answers with the new balance, or fails with the details of why it could not.
func Withdraw(balance: int, amount: int) -> int ! ShortOfFunds {
if amount > balance {
fail ShortOfFunds { balance: balance, requested: amount };
}
// Only reached when the money is there.
return balance - amount;
}
func Report(balance: int, amount: int) {
match Withdraw(balance, amount) {
.Success(left) => PrintLine("take {} from {}: {} left", amount, balance, left),
.Failure(error) => PrintLine("take {} from {}: refused, {} short", amount, balance,
error.requested - error.balance)
}
}
func Main() -> int {
Report(100, 30);
Report(100, 100);
Report(100, 130);
Report(0, 5);
// `fail` must carry a value of the error type named after the `!`. `fail 30;` inside
// `Withdraw` is rejected — "'fail' value must have type 'ShortOfFunds', but found 'int'".
// And `fail` belongs only in a function that can fail: here in `Main`, which returns a plain
// `int`, it is an error too — "'fail' needs an enclosing fallible function, but this
// function returns 'int'".
return 0;
}
Run it
cd Examples/Errors/Fail
rux run
take 30 from 100: 70 left
take 100 from 100: 0 left
take 130 from 100: refused, 30 short
take 5 from 0: refused, 5 short
Common mistakes
fail 30; inside Withdraw is rejected: error: 'fail' value must have type 'ShortOfFunds', but found 'int'. The error type is part of the signature, and every fail must match it.In
Main, which returns int, a fail is error: 'fail' needs an enclosing fallible function, but this function returns 'int'. The help line says what to do: declare the function's error channel, as in '-> T ! E' or '-> ! E'.Try it yourself
- Add
Report(100, 101);and predict the shortfall it prints. - Write
Deposit(balance: int, amount: int) -> int ! TooMuchthat refuses when the new balance would pass 1000, with aTooMuchstruct that says by how much. - Make
Withdrawrefuse a negative amount too. IsShortOfFundsthe right error for that? Error variant shows the better shape.
Learn more
- Unit fallible — a fallible function with no answer to give back
- Error variant — one error type for several ways to fail
- Absence to error —
failas the fallback of?? - Struct — the type the error is built from