Panics and assert: when a program must stop
On this page
Some failures are part of a program's ordinary life: a file that is not there, text that is not a number. A function reports those with a Result, and its caller decides what to do. Others mean the program itself is wrong: an index past the end, a promise broken. Going on would only compound the damage, so the program panics: it says what failed and where, and stops. This page covers panic, assert, the checks the compiler adds for you, and how to make a failure point at the line that caused it.
mod shelf;
import stdio use *;
// How many loaves are left on one of the bakery's shelves
fn stockOn(shelves &Array[i32], n usize) i32 {
assert(n < shelves.len, "there is no such shelf");
shelves[n];
}
fn main() {
let shelves = [12, 0, 7];
let first = stockOn(&shelves, 0usize);
print <- "shelf 0 holds ", first, "\n";
let missing = stockOn(&shelves, 5usize);
print <- "shelf 5 holds ", missing, "\n";
}
It prints:
shelf 0 holds 12 panic at shelf.cone:7: there is no such shelf
Try this: take the assert line out and run it again. The program still stops at shelf 5, now with the compiler's own message.
A bug report, not error handling#
The question to ask of any failure is whether a correct program could meet it. A missing file, yes: so the function that opens it returns a Result, and the caller must deal with both answers. Shelf 5, no: the bakery has three shelves, so asking for a fifth is a mistake in the program, and no caller can do anything sensible with it. That is what a panic is for. It is a bug report, written at the moment the bug shows, and it is never a way to handle an error.
The same goes for running out of memory, or reaching memory past the end of an array: there is no graceful way on. Two failures you might expect here do not panic: integer overflow wraps around silently (Numbers), and dividing by zero is not checked.
What a panic does#
The core library's panic panics with the message it is given, and assert panics when its condition is false. Either way, the program writes out whatever it has printed so far, then one line to standard error, naming the source file, the line, and the message, as above. Then it ends at once, through the C library's abort. On a thread other than the one the program started on, the line names it: panic in thread 5828 at ....
Nothing unwinds. No finalizer runs, no caller sees the panic, and the program's other threads end with it. That is a deliberate choice: since no function ever has to be ready to be unwound through, none carries the hidden code and tables that would take, and a lock held when a panic strikes needs no special state, because nothing will run that could wait for it (Locks). In Cone's design, keeping one failure from bringing down a whole program is the job of actors, whose memory can be discarded whole; that is not built yet.
The checks the compiler adds#
Some checks you never write. The compiler checks every index into an array, list or slice, and every range taken from one, and every allocation, and each failure panics the same way, naming what it compared:
panic at bounds.cone:9: index 3 is out of bounds for a count of 3
A range gives slice 2..<7 is out of bounds for a count of 5, and an allocation its region cannot satisfy gives out of memory allocating 24 bytes. A check costs its test and nothing more on the path where it passes; the report is built out of the way, only when it fails. On WebAssembly, which links no runtime to write the report, a failed check is a trap instead.
assert, and checks for debug builds only#
assert panics when its condition is false, with the message given, or with assertion failed if there is none. It is checked in every build, release builds included, so it is the right tool for a promise the program depends on:
assert(idx < bound); assert(idx < bound, "the index is within the table");
Some checks are too slow for a finished program, though: walking a whole table to confirm it is still in order, after every change. assertDebug is checked only in a debug build (conec --debug, and Congo's default unless it is given --release). In a release build the whole use is dropped and its condition is not evaluated, so the check may cost what it likes, and a condition with a side effect has it only in a debug build. assertDebugMsg is the same with a message of its own:
assertDebug(isSorted(&table)); assertDebugMsg(isSorted(&table), "the table is kept sorted");
With a table of [3, 9, 4] and an isSorted that prints a note as it checks, a debug build prints the note and then panic at adebug.cone:17: assertion failed, naming the line of the assertDebug; a release build prints neither, and carries on.
Both are macros in the core library, which is how the condition can go unevaluated: it is substituted where the macro is used rather than evaluated before a call. Each expands to an if on isDebugBuild(), a constant of the build that is true in a debug build, and a program may branch on it the same way. A macro takes no default argument, which is why the form with a message has a name of its own. The camelCase spelling of assertDebug is for now. A test can also expect a panic, or a debug-only check, on purpose: Testing shows how.
An earlier design had assert checked only in test builds, and a requires statement as the check that always runs. That role is now assert's; requires and ensures are kept for the contract conditions below, which are not built yet.
unreachable and todo#
Two more ways to panic say why in their name. unreachable() marks code the author knows cannot be reached, and todo() code not written yet. Each panics if it runs, with a message of its own or one given:
fn name(n i32) &str {
match n {
case 1 { "one"; }
case 2 { "two"; }
else { unreachable(); }
}
}
fn area(sides i32, size i32) i32 {
if sides == 4 { size * size; } else { todo("areas of other shapes"); }
}
area(3, 3) panics with panic at never.cone:18: areas of other shapes.
Never#
Notice that area gives back an i32, yet its else block holds no number. That works because todo, like panic and unreachable, returns Never: the type that says a call does not return. So such a call may end a block that must produce a value, as a return may, and the block has no value on that path:
let half = if n >= 0 { n / 2; } else { panic("negative"); };
A function of your own may return Never. It must then end in a call that does not return, or in an if each of whose paths does, and it may not say return. The C library's abort and exit return Never too. Wherever a value is asked of it, Never is void; it adds only that control does not come back.
fn fail(why &str) Never {
panic(why);
}
Pointing at the caller's line#
Look again at the first program's report: shelf.cone:7 is the assert inside stockOn. But stockOn did nothing wrong; the mistake was the call asking for shelf 5. The core library's own functions avoid this: a panic from assert names the line that called assert, not a line inside the core library. They do it with two parameters whose default values answer where the call taking them is, file &Array[u8] = srcFile() and line u32 = srcLine(). A function that panics on its caller's behalf may declare the same two and hand them on:
fn stockOn(shelves &Array[i32], n usize,
file &Array[u8] = srcFile(), line u32 = srcLine()) i32 {
assert(n < shelves.len, "there is no such shelf", file, line);
shelves[n];
}
Now the same mistake reports the line in main that asked for shelf 5. A message is text, a &str: a literal, or a String's view(). assertDebug and assertDebugMsg take neither parameter: being macros, a srcFile() or srcLine() in their body already answers where the macro is used (or, for a macro used inside another macro's body, where the outermost one is).
The panic hook#
A program that runs unattended may need its panics recorded somewhere besides standard error. setPanicHook names a function that a panic calls once its line is written and before the program ends, with the message and the location. The last hook set is the one called, from whichever thread panicked. A panic inside the hook ends the program at once.
fn logPanic(msg &str, file &str, line u32) {
eprint <- "logged: ", msg, " (", file, " line ", line, ")\n";
}
fn main() {
setPanicHook(&logPanic);
// ... the shelves, as above, with stockOn handing on its caller's line
let five = stockOn(&shelves, 5usize);
}
Both lines go to standard error, the panic's first:
panic at shelf2.cone:20: there is no such shelf logged: there is no such shelf (shelf2.cone line 20)
Contract conditions#
Not built yet. This is the design these pages first described, kept as it was written; the design of error handling now under way may rewrite it.
Where a function should preserve some invariants on entry and on exit, requires and ensures clauses may be added to its signature. They make the contract that governs the function's use visible, and stronger: if a condition fails, the program panics.
fn factorial(var n i32) i32
requires n > 0
ensures result > 0 {
var result = 1;
while n {
result *= n--;
}
result;
}
This function expects a positive, non-zero number, and returns one. Any other behaviour is a bug, to be caught quickly in testing and fixed.
You can now make a program stop loudly, at the right line, the moment it goes wrong, and keep the expensive checks out of the finished build. Next, the boundary where the compiler's own checks end: calling code written in C.