Errors · Lesson 9.15

Panic

Source
Stop the program at once with Core::Panic when something that cannot happen has happened.

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 ! EPanic
Is forsomething the world can do: bad input, a missing filesomething only a bug can do
The callermust handle it, and can recovernever sees it
The programcarries onstops at once
Cleanupruns, as on any returndoes 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.

Src/Main.rux
// 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

Forgetting #NoReturn.
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.
Panicking on bad input.
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.
Expecting cleanup to run.
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

  1. Change the month calculation to start + offset as uint and run the program. Compare the output with the listing above.
  2. Make DaysIn panic for month 0 with a message of its own, before the else arm.
  3. Write a #NoReturn() function Unreachable(where: char8[..]) that prints where and panics, and use it in another match.

Learn more