Panics

A panic stops the program at once. It is for conditions that cannot happen in a correct program — a broken invariant, an impossible case — where no caller could sensibly recover. A failure that a working program can meet, such as bad input or a missing file, is an error instead: a fallible the caller handles.

A fallible T ! EA panic
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
Cleanupdefers and destructors runnothing runs

Panic

Core::Panic stops the program with a message:

import Core::Panic;
import Io::PrintLine;

func Main() -> int {
    defer PrintLine("deferred");
    PrintLine("before");
    Panic("gave up");
    return 0;
}
before
Panic: gave up
  at Main (Src/Main.rux:7:5)

The report goes to the standard error stream: Panic: , the message, and the function, file, line and column of the call. The message is a char8[..]; to include a value in it, print the value first.

Panic never returns, so it may stand wherever a value is expected — as a match or catch arm, a ?? fallback, or a ? else mapper — without affecting the type of the result:

let trusted = ReadDigit('8') catch { e => Panic("a literal digit always reads") };
let value = Lookup(key) ?? Panic("the key is always present");

Assert and DebugAssert

An assertion is a panic with a condition attached. When the condition holds nothing happens; when it does not, the program stops:

import Core::Assert;
import Io::PrintLine;

func Median(scores: int[..]) -> int {
    Assert(scores.length > 0, "a median needs at least one score");
    return scores[scores.length / 2];
}

func Main() -> int {
    let scores = [3, 5, 8];
    PrintLine("{}", Median(scores));
    PrintLine("{}", Median(scores[0..0]));
    return 0;
}
5
Assertion failed: a median needs at least one score
  at Median (Src/Main.rux:5:5)

The condition must be a bool: Assert(scores.length, "…") is error: argument 1 to 'Assert' has type 'uint64', but parameter 'condition' requires 'bool8'. Write the message as what was expected — the failing value is not printed.

DebugAssert, also imported from Core, is the same check, kept only in builds with debug assertions:

AssertDebugAssert
Debug build (rux run)checkedchecked
Release build (rux run --release)checkedremoved — its arguments are not evaluated

A DebugAssert in a release build is gone entirely: a condition that calls a function does not call it, so any side effect of the condition disappears too. Keep the work outside the assertion and assert on its result. Whether debug assertions are on is the compile-time value #build.debugAssertions; see Context values.

Functions that never return

A function of your own can promise never to return with the #NoReturn() attribute. Like Panic, a call to it may then stand where a value is expected:

#NoReturn()
func Unreachable(what: char8[..]) {
    PrintLine("reached {}", what);
    Panic("unreachable code was reached");
}

func Days(month: int) -> int {
    return match month {
        2 => 28,
        4 => 30,
        6 => 30,
        9 => 30,
        11 => 30,
        1..=12 => 31,
        else => Unreachable("a month outside 1 to 12")
    };
}

A #NoReturn() function declares no return type (error: '#NoReturn' function cannot declare a return type) and may not contain return (error: return is not allowed in a '#NoReturn' function). Without the attribute, the else arm above would be an ordinary call that completes with (), and the match would be error: match arm type mismatch: expected 'int', found '()'.

Falling off the end.
A #NoReturn() function's body must end by panicking or by calling another function that never returns. rux 0.4.0 does not check this yet: a body that reaches its end stops the program there with no message.

Run-time checks

Some operations check their operands while the program runs, on every target and in every build profile, and panic when the check fails. Each report names the function, file, line and column of the operation:

ReportRaised by
Panic: division by zerointeger /, %, /= or %= with a zero divisor
Panic: division overflowsigned / or % of the type's minimum by -1, whose quotient does not fit
Panic: index out of rangea[i] on an array or slice with i not below the length; a[start..end] unless start <= end <= a.length
Panic: no match arm matched value of 'T'a match that the compiler accepted as exhaustive meets a value outside it, such as an enum value made with as from an integer that names no case
import Io::PrintLine;

func At(values: int[..], i: uint) -> int {
    return values[i];
}

func Main() -> int {
    let values = [1, 2, 3];
    PrintLine("{}", At(values, 3));
    return 0;
}
Panic: index out of range
  at At (Src/Main.rux:4:18)

A check the compiler can decide is decided at compile time instead. A constant index into a fixed array is checked when the program is compiled — fixed[7] on an int32[4] is error: index 7 is out of range for an array of 4 elements — and a division by a nonzero literal needs no check at all, and a release build removes checks it proves always pass. Raw-pointer indexing is never checked. Integer +, - and * wrap rather than panic; see Arithmetic.

What a panic does not do

A panic does not unwind. After the report the program stops on the spot:

  • no deferred statement runs — the deferred line in the first example is never printed;
  • no destructor runs, for any value in any function;
  • no caller observes the panic, and nothing can catch it.

That is deliberate: once a case that "cannot happen" has happened, the program's invariants no longer hold, and running cleanup code written on the assumption that they do can turn one failure into several. Anything that must be released on a failing path belongs on a fallible path, where the caller sees the failure and cleanup runs.

Exit status

A panic ends the process with an illegal-instruction trap, not through a normal exit. On Windows the exit status is 0xC000001D (STATUS_ILLEGAL_INSTRUCTION); on Linux, macOS and FreeBSD the process is killed by SIGILL, which a POSIX shell reports as status 132. Either way the status is non-zero, so a script or test runner sees the program as failed.

A fallible Main that fails is different: it is an ordinary exit with status 1, after cleanup.

See also