Panicking: Should you avoid it?

As with all things: it depends. Anyone who gives an answer without qualification of in what cases they're considering is at best overly confident at extrapolating their own experience.

BurntSushi's guideline is what I'll typically and happily point people at. Below is generally how I think about panicking. Warning: contains strained analogy of program tasks as ships.

To reiterate what's been said a couple times, the first guideline would be if it's relatively simple, generally prefer returning a meaningful Option or Result to panicking. This passes the question of how to handle the problem to the caller who likely has more context than you do about what that failure means. Or at least, they have a bigger picture view along with whatever context you gave them as the Result::Err case. This of course highly depends on the exact API you're implementing what this best should look like, but -> Result works fairly well for any high-level atomic operation, where either it happened successfully or it didn't happen and you can report what prevented it from happening.

The counterpoint to the above (as a category, generally "application error") is programmer error (also "logic error"), where the program has entered an unexpected state. Knowing nothing else, generally the best thing to do in this case is to panic!. If you've been asked to do something which doesn't make sense (e.g. index an object where there is no object at that index), a panic! is in essence a "controlled crash;" depending on application configuration, this should safely take down at least the task which panicked, along with perhaps the thread or entire application. If there's no way to accomplish what's been asked, panicking is the correct outcome.

The alternative which this controlled crash exists to prevent is the result of just attempting to do the thing which doesn't make sense anyway, producing at best unpredictable behavior as the program explores unintended code paths, and at worst the UB of doing things you've promised the compiler, optimizer, and virtual machine you'll never do, and at which point even the debugger may lie to you, because literally all bets are off. You don't want to visit the realm of UB, because nothing makes sense there.

What a panic! means depends on the application. A panic will typically display some sort of debugging message (this behavior is controlled by the panic hook and typically prints a message and optional stack trace to stderr) and the application gets to choose between -Cpanic=abort, in which case a panic then exists/aborts the program, and -Cpanic=unwind (the default), where the stack is unwound similar to how exceptions work in other languages, running destructors along the way to perform cleanup. When using -Cpanic=unwind, the application can use catch_unwind to terminate the unwinding process and do some sort of high task-level handling of the panic, such as logging the failure of that task and moving on to the next. If the application doesn't do this, an unwind takes down the thread, and the application as well if (and only if) that was the main thread.

For a library, then, you should always strive to provide an API that can be used without risking panics. Panicking APIs are often an ergonomic desire — for relatively simple preconditions like "index is in bounds," it is much simpler[1] not to have to continuously say "no, it's in bounds, I promise, I checked." But as a library, you should always offer some way at least to run a pre-check (where that's reasonable and doesn't suffer from <abbr title=time of check, time of use">TOCTOU issues) and ideally to get back Result or Option where more it's more complicated checks along the way.

On the other hand, you should always strive to avoid returning a default value indeterminable from a successful result. Program design isn't a game of social standing; you don't need to pretend nothing went wrong when you know it did. Nothing is worse than a program saying it's succeeded and hiding the fact that it didn't. At a high application-level loop, it makes sense to take failed tasks, discard them, and move on to the next thing. At any level other than that executor, minimally give your caller the chance to react to the fact you weren't able to accomplish the task you were told to do.

The thing about a panic! is that you're saying that the best response to what's gone wrong is to, well, panic, and just give up on whatever was currently being done. This is absolutely fine in many cases; programs are giant balls of messy state, and it's frankly a miracle that they stay in a reasonably functional one most of the time. If there's no good way to continue doing what you're supposed to be doing, panic!king is the correct course of action, because it would be worse to continue on a sinking ship than to admit it's going down and save what you can. In this strained analogy, that would be by panicking/unwinding, running Drop handlers to clean up state, and allowing other tasks (ships?) to continue without corrupting their state as well. Of course, the other ships tasks need to be prepared for you to panic and not panic themselves when they see your SOS and whatever state shared resources have been left in — lock poisoning exists to protect the other ships tasks from seeing potentially corrupted state caused by you panicking, typically by causing them to panic as well (via the .lock().unwrap()).

But in other cases, an error isn't a panicking matter; it's within expected operating procedure; you should record it and move on. In these cases, panicking is often an overreaction. But also, if what you are is just a oneshot CLI application, then a quick panic and exit can be a functional way to handle most errors, because the user at the CLI is better equipped to handle whatever it is (so long as you give them sufficient context).

What matters as a takeaway is that it always depends. All else being equal, it's generally preferable to give your caller more options by giving them a Result, but the situations where panicking is appropriate abound; all else is rarely equal.


  1. With my paranoid curmudgeon hat on, if you provide the panicking option, that also lessens the likelihood some over-confident idiot will use the Option version and utilize unsafe to unwrap_unchecked and upgrade a logic error into a safety issue in the name of performance because "I checked that, I promise, no need to check my work." I've been that idiot before and will again — it's for this reason I highly recommend any O(1) checkable unsafe precondition to be checked when cfg(debug_assertions) internally to any _unchecked APIs. Ideally with a noisy :warning::boom: emoji-laden panic screaming that this would've been UB in a release build. ↩︎

3 Likes