Use `catch_unwind` while panicking

Hello, everyone.

How to safely handle such situation:

Someone has written the library with type, which could panic while some cleanup operation

struct LibType {
}

impl LibType {
     fn cleanup(&mut self) {
          //some code which caused panic
         panic!("...");
     }
}
    

While using LibType I discoved that it could panic and would like to write something like this to prevent calling abort function in drop

struct MyType {
     dangerousField : LibType
}

impl Drop for MyType {
     fn drop(&mut self) {
         std::panic::catch_unwind( || {
             dangerousField.cleanup()
         })
     }
}

I understand that the author of LibType should make cleanup in LibType::drop and prevent the panicking and so on, but he/she is already make a mistake and I cannot fix it, because have no access to the code (let imagine it is distributed as propriatory crate).

As far as I know, this code still causes calling abort function because of double panicking:

fn test() {
      let t = MyType::new(); 
      panic!("Test!");
}

Is there any way to fix such problem?

C++ doesnt allow destructors to panic during unwinding. Because of this the native unwinder may not support this either. This means that rust cannot support it across different OSes, as it uses the native unwinder.

I'm not sure, that you're right about C++. Here is absolutely correct program: cpp.sh/2dgti

That is different: catch in cpp is like catch_unwind in rust. I meant throw inside a destructor, not inside catch.

It seems I cannot understand. Destructor in C++ should not throw exception by the same reason that destructor in Rust should not panic in drop - to avoid double throwing (which is causes std::terminate in C++ case).

But here is absolutely the same of the situation described in post which is written in C++ and everything fine with unwinding: cpp.sh/8lm7

Oops, in that case rust is to blame.

That's not sounds good and seems like a thing which is should be fixed in language :slight_smile: Such behaviour makes improssible to use RAII properly and be sure, that all resources in program will be freed on program closing.

Actually, Rust supports a Drop::drop implementation panic!-king:

What leads to an instant abort is if, during a panic!, when unwinding the stack and thus running the appropriate "destructors", one of these Drop::drop leads to a second panic!. In that case Rust does indeed abort.

  • fn main ()
    {
        use ::std::panic;
        let _ = panic::catch_unwind(|| {
            let _struct = Struct::new();
            panic!("First panic!");
            // panic leads to _struct.drop(),
            // which panics on its own, leading to an abort
        });
        // this is never reached
        println!("Program keeps going");
    }
    

If you intend to support a code pattern as the one shown above, i.e., to properly cleanup the Foo even while panic!-king, while not causing the process to abort so as to be catchable and reach further code, you will need to use closures / CPS so as to be able to "catch a caller's panic!", hold it, cleanup Foo with a second catch_unwind, and then resume the initial panic! so as to allow the caller to detect it and catch it:

fn main ()
{
    use ::std::panic;
    let _ = panic::catch_unwind(|| {
        Struct::with_new(|_struct: &mut Struct| {
            // use _struct here
            panic!("First panic!");
        }); // it is dropped here
    });
    println!("Program keeps going");
}
  • notice how instead of

    let mut _struct = Struct::new();
    // use _struct here
    drop(_struct); // or end of lexical scope
    

    we are doing

    Struct::with_new(|_struct| {
        // use _struct here
    });
    
    • This pattern is what allows to have guaranteed "cleanup" code (where Drop::drop is unreliable due to memory leaks

    • Playground

Click here to see the code of with_new()
impl Struct {
    pub(self) // private!!
    fn new ()
      -> Self
    {
        Self { foo: Foo }
    }

    pub
    fn with_new<R, F> (f: F)
      -> R
    where
        for<'any> F : FnOnce(&'any mut Self) -> R,
        F : panic::UnwindSafe,
    {
        let mut this = Self::new();
        let res = panic::catch_unwind({
            let mut this =
                panic::AssertUnwindSafe(&mut this)
            ;
            move || {
                f(&mut *this)
            }
        });
        // now drop `this`:
        match panic::catch_unwind({
            let mut foo =
                panic::AssertUnwindSafe(&mut this.foo)
            ;
            move || {
                foo.cleanup();
            }
        })
        {
            | Ok(()) => {},
            
            | Err(err) => {
                let err_msg: &dyn ::core::fmt::Display =
                    if let Some(s) = err.downcast_ref::<String>() {
                        s
                    } else {
                        &""
                    }
                ;
                eprintln!("foo.cleanup() panicked ({})", err_msg);
            },
        }
        match res {
            | Ok(ret) => ret,
            | Err(err) => panic::resume_unwind(err),
        }
    }
}
4 Likes

@Yandros Thank you, it looks like a solution... But this solution looks ugly! And it will be worse in real program which operates by many objects and all of them must use this pattern to be safe.

You also should keep in mind that even now the objects are written correctly, it could not be true in future so you should use such pattern on EVERY OBJECT and your code will become absolutely not readable.

It could more and more convinient dust wrap all code you not own in Drop::drop to catch_unwind and just log such things, just like catching all exceptions in C++ destructor, logging them and not crashing the software by one wrong-written object.

1 Like

Normal Rust code shouldn't panic. If some Drop impl is panic!()-ing that means that the world is in an indeterminate state, so there's not much the language runtime can do safely, other than crash loudly so a developer can come along and fix things. I feel like using this pattern for every object could lead to bugs or unpredictable behaviour...

I'd say the better solution is to review code and make sure it doesn't panic. Alternatively, if you are doing an operation in a Drop impl which may panic!(), you can use if !std::thread::panicking() to only execute that code when not already panicking.

2 Likes

If the world is in an indeterminate state, you should always use std::process::abort(), instead of panicking. Panicking should only be used when there theoretically is a sensible way to deal with the problem.

For example a browser running different tabs in different threads could keep the other tabs running when one tries to perform out of bounds indexing. Or a web server (for example hyper) could use a single thread for every request and return a 500 Internal server error when panicking and just keep handling new requests.

Normal Rust code shouldn't panic.

Where did you see a code which works as it should? :slight_smile: If you want to write reliable software, you should write code with understanding, that bugs exist. And bad programmers too :slight_smile:

you can use if !std::thread::panicking() to only execute that code when not already panicking.

I know about this feature, but someone who has written LibType::cleanup don't. And everything worked fine until one day, when the panic! branch in cleanup has occured.

The problem is in the fact, that I cannot avoid this situation. If something goes wrong in some thread I would like kill only the thread, not the whole process.

It is not right in programmer's point of view, but we're making software for users. Yes, after panic! we potentially have some indeterimnate state, but sometimes this state is not fair, and to handle such situation in rust we have, to put in mildly, not the best with_new solution.