Which one should I use? panic! vs. unreachable!

Another way to think about it: If a library you are using panics at an unreachable!, then that is a bug in the library, and not in your code, because that line of code should not be reachable in any way.

11 Likes

unreachable tells the reader that code in question shouldn't be ever executed. Like in the very example from the documentation:

fn foo(x: Option<i32>) {
    match x {
        Some(n) if n >= 0 => println!("Some(Non-negative)"),
        Some(n) if n <  0 => println!("Some(Negative)"),
        Some(_)           => unreachable!(), // compile error if commented out
        None              => println!("None")
    }
}

And it has stronger (and scarier) cousin: unreachable_unchecked. That one tells the compiler that you have done the math and may prove that this piece of code is never executed.

If you have done the math wrong… compiler reserves the right to destroy your program (not just the place where you call unreachable_unchecked but everything and anything!). unreachable! would just panic if you have made a mistake in your calculations so usually is a better choice unless you are writing really performance-critical code.

7 Likes
  • unreachable!(...) - if this line of code is ever reached, then this crate contains a bug, because it should be completely impossible for this to happen. Any message in here is there to help debug how the impossible was actually possible.

  • panic!(...) - if this line of code is reached, then a programmer has made a mistake that needs correcting. The message inside the panic!(...) macro is there to help the programmer fix it. Note that in the panic! case, the error may not be inside this code - as a simple example, Option::expect will call panic, but you might use it with clap::ArgMatches::values_o if you're confident that the argument was flagged as required; you know that if the argument is None, then either Clap has a bug, or you failed to tell Clap that the argument was required.

3 Likes

The documentation doesn't make a distinction between a bug in the library or a bug in the caller of the library. It simply states;

"This is useful any time that the compiler can’t determine that some code is unreachable."

It seems to me a library that uses unreachable due to a caller error cannot be definitely said to be incorrect. That said, the examples given are entirely local in nature ( that is the determination that the code is unreachable can be made by examining only the containing function ). I think it's reasonable to use unreachable! in any situation where the code should never be executed. But then, panic! becomes redundant ( I think ). Perhaps it is a matter of judgement.

3 Likes

I agree that the documentation isn't clear enough, but I still think it's a bug if the unreachable!() is reachable by any means. It's more clear with the message it prints.

macro_rules! unreachable {
    () => ({
        $crate::panic!("internal error: entered unreachable code")
    });
    ($msg:expr $(,)?) => ({
        $crate::unreachable!("{}", $msg)
    });
    ($fmt:expr, $($arg:tt)*) => ({
        $crate::panic!($crate::concat!("internal error: entered unreachable code: ", $fmt), $($arg)*)
    });
}
3 Likes

Ok, then the question becomes "internal" to what? The function? The module? The crate? The whole program?

I would tend to agree that "The whole program" is an unreasonable interpretation, because panic! then apparently becomes redundant.

3 Likes

I am even more confused :rofl:.
Could I replace unreachable! with panic! generally?

2 Likes

It depends what you're writing. If you're writing some library, at this point you have no idea what would be "The whole program". I think it's hard to say the "internal" means that in this case.

2 Likes

unreachable! quite literally inserts panic! with a certain text.

So technically you can, of course. It's more about code smells than about technical correctness.

P.S. Note: unreachable_unchecked is completely different in that regard: if an attempt to “execute” that code would happen then program would misbehave in very bad way. It shouldn't ever happen, no matter what data is supplied to your module. I think unreachable! should be used in a similar fashion: yes, this code shouldn't be ever used, but put panic! there just to be double-sure. If that panic!is ever shown to the end-user that means that software developers failed to develop program properly.

4 Likes

Nice! I got it.

2 Likes

It might be easier to give examples where panic! should be used rather than unreachable!.

One example would be overflow or other limits - supposing your program uses a 16 bit number to represent some number, but the limit is exceeded. That could be a panic! situation rather than unreachable!. Apart from the differences in the message printed, they are basically the same.

2 Likes

panic!() is the standard library macro to panic with an arbitrary message — it is the most general (and thus most non-specific) tool available in this area.

unreachable!() is just one of several macros that are for documentation and error message purposes, more specific than panic!(). The others are todo!() and unimplemented!(). (One could consider assert!() also in this family, though it has the additional feature of testing a condition.) All of them have the primary effect of panicking — they don't change what your program does, really, only how it explains itself. (Unless something is inspecting the text of panic messages, which is a sign that you should almost certainly be using Result instead of panicking.)

So, I think the question of “which one should I use” should be reformulated depending on where you're coming from:

  • If you have concluded that a particular erroneous situation should be handled by panicking, then the question is: is the situation best described as an unexpectedly reached code path, one that you thought shouldn't ever happen? Then use unreachable!(), because that's what it's for. Otherwise, use one of the other panicking macros.

    This case isn't really a critical decision; it's in the family of "code comments, documentation, good error messages…".

  • If you have found that you need to fill in some code for a situation that you think can't be reached (e.g. _ => ... in a match that you can't make straightforwardly exhaustive and always successful) then you should consider how likely it is that you're wrong.

    • If you're writing a library and the user of the library could cause this to happen, it might be good to provide an error-reporting option other than panicking (i.e. Result).
    • If you are fairly confident that it can't happen any way at all unless there's a bug, then use unreachable!() to fill in that hole.
    • If you need more performance and the code is simple enough to fully understand, really truly so, then maybe use unreachable_unchecked!() — and benchmark to check that it is faster that way, and run a fuzzer to help confirm that it isn't actually reachable, and think twice about doing this at all, because undefined behavior hurts.
9 Likes

I try to only call unreachable! in places where unreachable_unchecked() should be correct. Due to the problems that can occur if I get the reachability analysis wrong, I’ll only ever use unreachable_unchecked if I’ve identified an actual performance problem arising from an existing unreachable! call, and I’ll re-verify that the line is actually not reachable.

7 Likes

Thanks. Same as I wanted to say. For me unreachable! is yes, this shouldn't ever be executed, but please don't ask me to spend two weeks playing with debugger if that would ever happen (like unreachable_unchecked would do) but print a nice message.

2 Likes

@2e71828 This is a great way to go about it, same as what I do. I believe where there's even some situations where the compiler can prove it's truly unreachable and optimize it away itself, but even if it can't I usually don't think unreachable_unchecked is worth the risk of the compiler destroying my program, itself, me, my house, society, or the whole universe (which is technically allowed, if infeasible)

1 Like

Yes, you can always use a plain panic! instead of unreachable!.

panic! is the bottom-level primitive; it just tells the compiler "if this line of code is reached, abort the program because there's nothing that can sensibly be done here - it's a bug somewhere in some of the code that's part of this prgram".

unreachable! is a way to call panic! that adds "and when the programmer comes and looks at this failure, they need to know that it should not have been possible to reach this line of code, and the fact that this code has been reached means that a past programmer's belief in what is (and is not) possible was proven false".

todo! is a way to call panic! that adds "and when the programmer comes and looks at this failure, they need to know that the program did something reasonable, but that nobody has yet written code to handle this state."

unimplemented! is a way to call panic! that adds "and when the programmer sees this failure, they should know that it failed because nobody has implemented this code path". The difference with todo! is that it may or may not be reasonable to write code to handle this state - with todo!, a past programmer thought that handling this state was reasonable, with unimplemented!, they just didn't write code to handle it and you have no further clue.

Remember that your hardest job as a skilled programmer is to write code that a future programmer can maintain - while you could use panic! everywhere, the advantage of the other macros is that they give the future programmer (possibly you!) a clue about what you were thinking at the time you wrote the code in question.

If I tell you that my code has failed at the line panic!();, you know very little about what's going on. If I tell you it's failed at unreachable!();, you have a bit more of an explanation. If I tell you it failed at unreachable!("only drop should take the Option contents");, you know a lot more about what went wrong.

Similar with todo!(); if I tell you that my code failed at todo!("handle IPv4 IP Options header");, you know that this is perfectly reasonable code to write, I just didn't write it; if I write unimplemented!("no code handles IPv4 IP Options"); you have slightly less information, because I may think that handling IPv4 IP Options is not worth the complexity.

As a final note, compiler authors tend to do their best to make easy to read code perform well, too. You should normally focus on making your code easy for a human to read, and only worry about making it easy for the compiler to turn into the "best" code when you have proof that the compiler is doing a bad job with the easy to read code. Otherwise, you run the risk of wasting time doing things that the compiler already does, resulting in code that's harder to read and no faster than the clear version.

6 Likes

Panics can be handled, I think it's important to be clear about this, as some people may not be aware of this useful possibility ( at one stage in the past I didn't know, and wasted a fair bit of effort as a result). catch_unwind in std::panic - Rust

1 Like

You (learners of Rust) should also be aware that this is not, in the general case, true. Programs can be compiled to abort on panic, and that is a choice the compiling party makes, not the code author. There are also a lot of tricky caveats around catching panics that even experts get wrong.

It's applicable in niche circumstances, but don't get the impression that catching panics is something you should reach for lightly, or rely on always happening.

Hashed out previously in this thread and others.

3 Likes

They can sometimes be handled. As per the docs on catch_unwind, it only catches "unwinding" panics, but you can also have aborting panics which cannot be handled.

Currently, there are two ways to get aborting panics:

  1. Compile with panic=abort, which makes all panics aborting.
  2. Call std::panic::always_abort which makes any future panics aborting panics.

Additionally, a panic hook set by set_hook can take action before the panic runtime has a chance to run. If it exits (as that playground example does), then your catch_unwind never returns. It could also loop forever, again stopping you catching the panic.

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.