Some things I learnt in the last few weeks

Exceptions have an associated object that can be manipulated when they're caught.
For example:

class Foo {
    void foo() {
        throw Exception("whoops");
    }

    void bar() {
        try {
            this.foo();
        } catch (Exception e) {
            // stuff can be done with `e`
        }
    }
}

Haven't seen panics allow that thus far, not that I'm advocating for that.

fn panicky() {
    std::panic::panic_any("hello!".to_owned())
}

fn main() {
    if let Err(payload) = std::panic::catch_unwind(panicky) {
        println!("{}", payload.downcast::<String>().expect("wrong payload"));
    }
}

The type of payload is Box<dyn Any + Send + 'static>, which is not too different from an opaque Exception.

5 Likes

Then it seems that, when used as a recoverable fatal error (still a contradictio in terminis if you ask me), panics really do almost behave like exceptions. The almost comes from the switch allowing the recoverability to be turned off, which I still contend makes them the less reliable option.

I guess I would say that "panic = fatal error" is a (strong) convention, not something built into the language. You can certainly argue that because of various properties of panics they are best suited to error conditions that are not expected to be handled further up the call stack.

FWIW, GCC also supports the equivalent of panic = "abort" for C++ exceptions. Well, it's not exactly equivalent—passing -fno-exceptions causes try { ... } catch and throw to be compile errors.

I think there are valid use cases. If you have a thread-pool for example, you can catch panics in order to avoid losing (and needing to re-create) any worker threads. If you have a long-running application, you can add a recovery system, catching panics if any occur, to avoid shutting down the service – even if panics are never actually expected to happen. I.e. if your program happens to have a logic bug somewhere triggering a panic in certain situations, catching a panic allows you to avoid a small bug taking down the whole application.

Also, threads. If you spawn a thread, a panic from this thread will just kill that single thread. You can observe this from a different thread via the JoinHandle. Hence, even without catch_unwind, one could use threads for a (less efficient) way to catch panics. In fact, thread::spawn uses catch_unwind internally (here). There’s not really any good alternative option since unwinding can’t work across threads for technical reasons, so there always has to be a way to catch panics in Rust, if you want Rust to support unwinding. And unwinding itself is useful to free up resources properly with destructors.

That’s another big use-case for catching panics: Free up some resources, then re-throw the panic. Admitted, it’s nicer to use destructors for this usually, but – again – for multi-threading, using .join().unwrap() is the way to get parent threads fail together with their child threads in a way that cleans up resources, and – as mentioned – catch_unwind is used internally in order to achieve this.

I suppose, a similar use-case is (manually) propagating unwinding across FFI-boundaries. Maybe – again – spawning threads qualifies as a special-case of this since it’s using e.g. pthread_create on Linux, and you wouldn’t want the callback passed to pthread_create to unwind across FFI-boundaries. I can imagine other FFI-bindings involving callbacks would need to do a similar thing and use catch_unwind inside of the callback, and then perhaps even re-throw the panic at a different place.

5 Likes

I think a lot of these valid uses can be simply restated:

When a library calls into long-running user code, it should catch_unwind at the transition point.

For Java developers, think about when you would } catch (Throwable t) { and actually have a recovery plan: exactly the same use cases.

Well, in terms of implementation they're using exactly the same stuff as C++ exceptions, so...

I think it's hard to do robustly, though that particular footgun may be removed through other means eventually. In the meanwhile, I believe that if you're going to drop any part of the payload you should use another catch_unwind! (or leak the payload). But doing this is rare. As it turns out, reasoning about panics is hard, even in code explicitly about catching panics, because of their often-invisible nature.

That said, you're also getting push-back because it's not idiomatic Rust and it's (excuse the pun) an exceptional code pattern, not an expected one. It is not what you should reach for as part of normal error handling, and it makes addressing the error cases invisible and forgettable -- anti-patterns in Rust.

So when an Rustacean reads "[As I Rust beginner I recently learned that panics] may be worth considering an an alternative to Result", they're probably going to jump in and correct you. Not just to be pedantic, but also to guide you toward idiomatic Rust and away from footguns.


More generally, this feels like one of those mid-level issues, like self-referential structures or falling back to raw pointers (or falling back to unsafe generally). Are they possible? Yes. Should you learn them and should we have better material about them? Eventually you should and we definitely could use some better or centralized material on some topics. Are they what you should be reaching for in your first projects and while you're still getting a feel for the language? Almost certainly not.

14 Likes

Yes, but I feel the beginners mistake I made was to use Result when Panic was a superior solution for my use case ( more readable code, less chance of coding mistakes ). I have a bad memory, so I am not sure what I believed originally, but I don't think I knew panics could be caught, or didn't consider that as a solution. My mistake!

1 Like

Before you get too confident about your apparently-uncommon skill at safely handling Panic, I suggest that you read the preprint of the just-announced on IRLO paper RUDRA: Finding Memory Safety Bugs in Rust at the Ecosystem Scale. Figure 3 in that paper is an example of a panic safety bug in String::retain() that RUDRA found in stable Rust's standard library.

Also see Table 4, "Details of the new bugs found in the 30 most popular packages on crates.io", which includes three more panic-handling safety bugs. As the rest of that table title states,

The RUDRA paper provides quite-convincing evidence that safely handling panics is extremely difficult, even for very experienced Rust programmers, including those who maintain rustc and std.

12 Likes

This is very interesting, thank you for reposting it here, I don't often go to IRLO.

2 Likes

Note that this bug relates to panic handling in the context of writing unsafe code—modifying the bytes of a str—which has many more pitfalls than panic handling in safe application code (which I assume is the context @geebee22 is operating in).

3 Likes

That is true. However, if the Rust ecosystem contains so many potentially-flawed responses when a panic does occur, then it might be wise to avoid that exposure by not deliberately inducing panics as a programming strategy. (Of course that exposure only occurs when one expects to recover from the panic.)

3 Likes

There is no special skill involved. It's necessary to make sure everything is in a consistent state before continuing (after reporting the error), ( "panic::AssertUnwindSafe" ) but that's not difficult in my situation. I am only writing in safe Rust, not venturing into truly dangerous waters!

1 Like

You can catch panics, but you can't assume your panics can be caught, especially if you're writing libraries. Danger of double panics are already mentioned, and it's not that rare people compile their binaries in panic = "abort" mode, which abort the process immediately after the panic without stack unwinding.

3 Likes

Note: The same author posted a separate thread all about panic and catch_unwind. Some of this commentary might fit better in that thread, or has already been covered there.

1 Like

:neutral_face: I don’t know very well, can you provide a more detailed sample code description? Thank you.

Some more information and examples about T: 'static vs &'static T can be found in

rust-blog/common-rust-lifetime-misconceptions.md at master · pretzelhammer/rust-blog · GitHub

in particular in sections 2) and 3)

1 Like

I see this thread got some traction over panics.

I'd like to add to your list std::mem::take() which, given a type that implements default replaces mut ref with default and gives you the referenced value. Much like Option::take(). I use that a lot in situations with async Sync and Send implementations. Not sure if this is in the book :slight_smile:

I'm also fond of async traits and figured I don't really need to depend on async-trait crate for that (and the bunch of proc macro deps). This is my typical use case. On top of async-trait, it allows me to achieve a Sync future, or do some preparation before the async code. It is a bit verbose, but with a bit of practice it actually helps one really understand and get the knack for lifetimes.

2 Likes

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.