Panicking: Should you avoid it?

Heyo!

Been researching panicking lately, and I wanted to ask, should it be a goal to have code (e.g. for a game) that won't ever panic, or is that unrealistic?

If it is realistic, what are some good practices to achieve that, and if not, what would be a realistic goal?

Note for future readers: while I marked a reply as the "solution" because it had a lot of good information, there are still other helpful replies below

4 Likes

To my mind a panic is what happens when your program hits some erroneous condition that the programmer has failed to anticipate and handle. Basically a bug. Or something has happened that is bad and unavoidable, like running out of memory. At such a point your program is in an unknown state and the best thing to do is bail out immediately rather than try and limp along in a broken and unpredictable way.

You may like to read this lengthy discussion of panic Negative views on Rust: panicking

3 Likes

Thanks for the reply!

I've actually already read through most of it, but as far as I could tell the discussion was more about if it's well designed, than how you should handle it in your code

Panic is one of the best scenarios after hitting some bugs or unexpected cases. Should it be a goal to have code that doesn't have bugs? Absolutely. Is it realistic? Absolutely not. Should you avoid panicking even in case of bugs? Well, it can only makes things worse.

7 Likes

Just to check I'm not misunderstanding, you're just implying that e. g. replacing .get(i) with .get_unchecked(i) for the sake of “avoiding panics” is not a good idea, right? Or to name another example: removing an assert!ion that isn't necessary to prevent UB, but protects against logic errors is a bad idea, if done for the sake of “avoiding panics”. Or... giving some practically nonsensical fallback return values in case of “invalid” inputs to a function for the sake of avoiding panics. (E. g. writing a function that gives you the largest (or maybe smallest) element of a list of integers (as a non-Option return type, but making it return zero for the empty list, so you don't need to panic. Panic would be better here; and Option return type possible even more so.)

3 Likes

"Avoiding panic" can mean different things:

  • Aborting on panicking, such that the program will not be in panicking state ever
  • Explicitly aborting instead of panicking in case of certain critical errors
  • Avoiding programming errors that cause a panic
  • Not handling certain programming errors and allowing undefined behavior (UB)
  • Returning an error value instead of panicking
  • Returning an errorneous (possibly unspecified) value that does not allow error checking in case of errors

All these are justified in certain scenarios. For example, the last one happens with the --release profile on integer overflow. Accepting UB in case of a programming error may be okay in thoroughly reviewed time-critical code. And Rust does abort in case of a panic in a drop handler, right?

So to discuss this issue properly, we must be sure to talk about the same. What is really meant with "avoiding panics"?

4 Likes

My suggested philosophy: you're allowed to write code that calls panic! (or unwrap or …) so long as you consider it bug-worthy if a user can reproduce hitting it.

This assumes you can ship fixes at reasonable cadence, or are willing to have sufficient testing to be sure users won't hit them in supported scenarios.

You also need to make sure that you don't corrupt user data should something panic, but you need to do that anyway because it's infeasible to be certain your code won't panic. (And you need to deal with getting killed or run out of power or whatever too.)

Untested "stumble along" code is often more risky than just panicking. If something happened you thought couldn't, the program getting killed is good.

(I used to work at a place that had a "crash unless" macro, and using that pervasively actually increased reliability. Was easier to debug, too, since the bugs got crash dumps.)

13 Likes

Panicking: Should you avoid it?

The short answer is: "yes".

The long answer is: "it depends", i.e., it depends on what you mean by "avoiding".

In general, it is advisable to try handling errors as hard as you can. This entails returning semantic types indicating error conditions, e.g. Result for denoting the failure of a fallible operation or Option for representing the absence of a value or meaningful answer.

You shouldn't generally "trust" users or their input and assume that only the happy path will occur. When you are designing some code, and especially in the presence of external-facing APIs, you should make it hard or ideally impossible for your code to be used incorrectly. People won't expect your code to panic. In fact, nobody expects code to hit control flow that is not apparent from the structure of the code – this is why implicitly-thrown, untyped "exceptions" cause preposterous errors and dormant bugs in languages without real error handling.

Furthermore, panics can't be caught reliably even if you do remember them (e.g. panic = "abort" profiles). This means that if you decide you want to panic on error, you have completely taken away the ability of graceful error handling from downstream consumers of the code. This is, needless to say, Bad™, and you shouldn't design your APIs like that by default.

However, in general, it's not possible to always avoid panicking. Maybe you are yourself a consumer of a bad API that just panics left and right, or you have an invariant in your code that is impossible to encode in the type system, so you can't have the compiler prove that "element at index 1 will always exist", or something like that. In this kind of situation, it's better to panic than to silently misbehave. It's better to have your program crash cleanly at the determinate point where it performed an invalid operation, than have it access uninitialized memory, return garbage answers, or accidentally launch the nukes.

So, sometimes, panicking is the better alternative. But that's a pretty rare special case that you shouldn't be hitting too often. Try to use the type system for upholding invariants as your first attempt, and only if that doesn't work out, resort to runtime checking and panicking.

11 Likes

I agree with others that your question is a little under-specified. But this question, and many similar ones like it, is why I wrote this blog: Using unwrap() in Rust is Okay - Andrew Gallant's Blog

(From what I can tell, it seems pretty consistent with the advice you've gotten so far. But it spells it out in more detail and with real world examples.)

10 Likes

Heyo everyone, thank you for the replies! I will have to think on this.

In response to the "what do you mean by avoiding?" Questions, I apologize. I am a beginner and will be prone to these mistakes as I only barely know what I'm talking about lol.

To answer, in my initial question I meant "avoiding programmer errors that cause panicking", to quote jbe

As others have said, panics should be reserved for unrecoverable bugs. So your question becomes "how do I avoid bugs". Most commonly you write tests. Ordinary unit and integration tests are the most common ones.

Fuzzing can help to automatically generate random inputs, which are quite likely to hit various unlikely edge cases. Different fuzzing techniques typically hit different edge cases, and not every problem is amenable to fuzzing (e.g. if you have some internal consistency checks on the data, like a checksum, then you won't normally generate the interesting cases).

Property testing is easier to use and to understand than general fuzzing. I commonly write proptests instead of fixed unit tests. Generating the data may require implementing the Arbitrary trait, or composing the proper data pipeline.

Formal verification is harder to use, but can give you stronger guarantees. I have some mildly positive experience eliminating runtime panics with kani model checker. Prusti is another popular alternative. Generally, different tools have different limitations, so you should try and find the one that works best for you.

9 Likes

I'm wondering what you mean there? Surely if the code is "thoroughly reviewed" we would expect it not to have UB, that is the point of the review right? And if it still does a panic would be nice, rather than random behaviour. I can't imagine accepting UB as being OK in any circumstance.

4 Likes

It is ok to panic on unhandleable errors, but usually not ok when you can actually handle your error.

P.S. Looks like my reply is pretty useless…

My impression is that if you have an unhandleable error, for example an out of memory error, then it's best to quit immediately. Things are out of your control. That is a panic.

Many other panics show you have a bug in your code, for example, a divide by zero error, an out of bounds array access, in which case it's better to quit immediately with a panic. Then you know you have a bug to fix and where it is happening.

When thinking about errors it's useful to forget about the error free "happy path", think about all the error/failure paths, design your program to respond to them as you want. Those error paths deserve as much, perhaps more, attention in the design of your program than the job you actually want to do.

To your actual question. Consider opening a file for reading that does not exist. Something like fs::read_to_string() returns a Result which may contain an Error. That error maybe fatal, say it is trying to read a required configuration file, so perhaps best to check for that error, print a suitable message and exit the program. Not with a panic though. Or it might be recoverable, say the user typed a file name incorrectly, so you may want to prompt for a new file name and try again.

5 Likes

Note the "in case of a programming error", i.e. the review should avoid a programming error, but by writing unsafe, we tell the compiler that in case of a programming error, we will cause UB.

That's what I meant. Not that UB is good or should happen. But if we use unsafe, it may happen in case of programming errors (e.g. instead of panics).

Isn't that just definition of unsafe? Code should be marked as unsafe when mistakes in it lead to UB (which is by definition worse than panic because it's unpredictable) — but as a compensation you get “superpowers”.

1 Like

To be more precise, this only applies to unsafe { … } blocks as well as unsafe impl, not the uses of the same token “unsafe” with the dual meaning, i.e. unsafe fn[1] and unsafe trait.

Also, it’s usually not true that the code where mistakes can lead to UB is limited to the code inside of unsafe blocks (or unsafe impls); commonly the unsafe block is only used directly where the “superpowers” are needed, whereas non-unsafe code around it – or in other places – can be relevant for safety of those unsafe blocks, too. This can even apply e.g. to (non-unsafe) standard library code… one might write an unsafe block whose soundness might rely on the well-behavedness (i.e. bug-freeness, even if those bugs were just “logic errors” and not direct UB in and by themselves) of standard library collection types… by using a Vec or VecDeque or HashMap and expecting those to be well-behaved.


  1. well… those do both, wrapping their bodies into an implicit unsafe { … } block, too ↩︎

4 Likes

I am also new to Rust and programming is just a hobby :slight_smile:
So, my answer may not be correct from practical point of programming, but my 2 cents would be:
Don't panic if you can return error.

Most of time everything depends at your situation and application.

Here where few answers who said you should panic if you try to get out of index memory. But I would say you don't need to panic, you need to return Option::None. Rust have very good data type std::option, so if it is possible to have out of index situation you just wrap everything in Option and return None on error.
Or you can return std::result if here can be more different errors, not just out of index.

In this case code user will need to make a decision to panic, fix problem or to return std::result with error to higher caller.

panic also close your application instantly, but your application may be able to save user data before closing in this case panicking makes user lose data. It is very annoying. I know. I use AutoCAD and it often close for now reason and I lose half day work. Right now they changed a little bit and more often lets save drawing before closing application.

Even out of memory problems sometimes can be solved, depending at application. For example you have multi document application like AutoCAD. One document take 1GB of RAM and PC have 8GB RAM. So, if your application opens 8 documents and will try to open 9 document you will get out of memory error, but in this situation it would be stupid to panic and close application losing all not saved data of other 8 documents. It would be much better to just abort opening or creating new document. Because here are nothing wrong with other documents, you just don't have memory for new document. If user saves and closes one or more of documents you can open new document.

So, it mostly depends if your application can fix problem or can ignore it. If your application is multi document application, is problem related to specific document or to whole application and many more questions.
Everything comes to architecture of your application.

1 Like

Wise words. Worth far more than 2 cents.

1 Like

P.S.
I have read "the Book", "rust By Example" and many other books on internet and in one of them author wrote.
Use panic only in Tests, your release code should have 0 panic. Your test codes can have as many panics as you want/need.

How I view panic in release code:
You work with knife. To you come your client and accidentally he cut his finger of. You don't know how attach his finger or stop bleeding. So, you panic, take your gun and shoot your client in his head. But just outside your shop here was medics who could not only stop bleeding, but even attach his finger back. But you come to decision, if you can't fix problem, no one can fix it, so you just killed him.

Don't take responsibility, return it to someone who made order. He may be able to fix it. You only responsible for your job, if you can do job with data client give you just say it to client, don't kill client because of bad data.