Panic
A fallible T ! E is for failures a caller can do something about: a bad number typed by a user, a file that is missing. Some situations are not like that. They cannot happen unless the program itself is wrong, and no caller could sensibly recover. For those there is Core::Panic(message).
Failures and bugs
The question to ask is: could this happen in a program with no bugs?
A fallible T ! E | Panic | |
|---|---|---|
| Is for | something the world can do: bad input, a missing file | something only a bug can do |
| The caller | must handle it, and can recover | never sees it |
| The program | carries on | stops at once |
| Cleanup | runs, as on any return | does not run |
A month number of 13 typed by a user is a failure: tell them and ask again. A month number of 13 computed by % 12 + 1 is a bug: the arithmetic is wrong, and nothing the caller could do would fix it.
What Panic does
Panic prints its message and where it was called, on the error stream, then stops the program at once. Nothing unwinds: no cleanup runs, no caller gets a chance to catch it, and the program exits with a failure status. That is on purpose — once something "impossible" has happened, the program's assumptions are broken, and running more of its code would only spread the damage.
This program never panics, but change the month calculation to start + offset as uint and the third month is 13. The output then ends like this, and nothing after it runs:
month 11: 30 days
month 12: 31 days
month 13 does not exist
Panic: a month is always 1 to 12
at BadMonth (Src/Main.rux:30:5)
Rux panics the same way on its own when an array index is out of range: the program stops with Panic: index out of range and the place of the bad subscript.
Panic never returns
Because Panic never returns, it may stand where a value is expected. Every arm of DaysIn's match must produce an int, and the else arm produces none — it never finishes:
return match month {
2 => 28,
4 => 30,
6 => 30,
9 => 30,
11 => 30,
1..=12 => 31,
else => BadMonth(month)
};
The 1..=12 range pattern gives every other real month 31 days, so the else arm is reached only by a month that does not exist.
#NoReturn for your own functions
A function of your own can promise the same with the #NoReturn() attribute. BadMonth uses that to print the offending value, which a plain Panic message cannot include, before it panics:
#NoReturn()
func BadMonth(month: uint) {
PrintLine("month {} does not exist", month);
Panic("a month is always 1 to 12");
}
Without the attribute, BadMonth looks like an ordinary function that returns nothing, and the else arm no longer produces an int.
The program
The whole lesson is one package in the Examples repository. Its comments explain every step.
// A fallible `T ! E` is for failures a caller can do something about: a bad
// number typed by a user, a file that is missing. Some situations are not like
// that. They cannot happen unless the program itself is wrong, and no caller
// could sensibly recover. For those there is `Core::Panic(message)`.
//
// `Panic` prints its message and where it was called, then stops the program
// at once. Nothing unwinds: no cleanup runs, no caller gets a chance to catch
// it, and the program exits with a failure status. That is on purpose — once
// something "impossible" has happened, the program's assumptions are broken,
// and running more of its code would only spread the damage.
//
// `Panic` never returns, so it may stand where a value is expected, like the
// `else` arm below. A function of your own can promise the same with the
// `#NoReturn()` attribute. `BadMonth` uses that to print the offending value,
// which a plain `Panic` message cannot include, before it panics.
//
// This program never panics: `% 12 + 1` keeps every month between 1 and 12.
// If `BadMonth` were reached, the output would end like this, and nothing
// after it would run:
//
// month 13 does not exist
// Panic: a month is always 1 to 12
// at BadMonth (Src/Main.rux:30:5)
import Core::Panic;
import Io::PrintLine;
#NoReturn()
func BadMonth(month: uint) {
PrintLine("month {} does not exist", month);
Panic("a month is always 1 to 12");
}
func DaysIn(month: uint) -> int {
return match month {
2 => 28,
4 => 30,
6 => 30,
9 => 30,
11 => 30,
1..=12 => 31,
else => BadMonth(month)
};
}
func Main() -> int {
// A loan that starts in November, counted month by month.
let start: uint = 11;
var total = 0;
for offset in 0..4 {
let month = (start - 1 + offset as uint) % 12 + 1;
let days = DaysIn(month);
total += days;
PrintLine("month {:2}: {} days", month, days);
}
PrintLine("total: {} days", total);
return 0;
}
Besides Io, its Rux.toml lists Core under [Dependencies].
Run it
cd Examples/Errors/Panic
rux run
month 11: 30 days
month 12: 31 days
month 1: 31 days
month 2: 28 days
total: 120 days
Common mistakes
Remove the attribute and
DaysIn stops compiling: error: match arm type mismatch: expected 'int', found '()'. The compiler no longer knows that BadMonth never comes back.Input from a user, a file or the network can be wrong in a working program. Report that with a fallible so the caller can recover; keep
Panic for the cases only a bug can reach.A panic stops the program at once. A
defer that would run on a return or a failed ? does not run on a panic.Try it yourself
- Change the month calculation to
start + offset as uintand run the program. Compare the output with the listing above. - Make
DaysInpanic for month 0 with a message of its own, before theelsearm. - Write a
#NoReturn()functionUnreachable(where: char8[..])that printswhereand panics, and use it in anothermatch.
Learn more
- Panic in the API reference
- Fatal errors and NoReturn in the Rux Reference
- Assert — a panic with a condition attached
- Fallible — for failures a caller can handle