Home > Back-end >  What happens if an exception code path throws another exception?
What happens if an exception code path throws another exception?

Time:11-29

I encountered some code at work today where an exception is thrown from a destructor.

  • You should never throw an exception from a destructor. I point this out because I'm sure if I don't, someone else will.

I am informed that this was a concious design decision and is needed to clean up some data in the case where another failure is encountered. The process of stack-unwinding is being exploited to do the clean up. Under normal circumstances the clean-up process is successful and no exception is thrown. However today I encountered a case where the clean-up failed and an exception is thrown, hence I began to investigate.

The above aside, since this is not a question about code-reviewing code the organization I work for uses, my question is as follows.

What happens if the code path followed as the result of throwing an exception throws another exception?

Since this is an unsual situation I only know of 2 ways this can happen.

  1. The first is the trivial case where an exception is thrown, it is caught by a catch block, and that then throws. This is the same as just throwing an exception which is not caught. There are already some questions about this, for example here. In short, terminate() is called.

  2. When an exception is thrown, the Stack Unwinding process begins. This process calls destructors of stack allocated objects. Therefore the only other way I know of to cause the throwing of nested exceptions is to throw inside a destructor, in the same manner as I encountered today.

I cannot think of any further possibilities. If there are any I would be interested to hear of them.

Regarding point 2. What happens in this case?

CodePudding user response:

What happens if the code path followed as the result of throwing an exception throws another exception?

That depends on where and when exactly is the second exception triggered. In C , you can have an "unlimited" amount of currently thrown uncaught exceptions and everything must work just fine. This makes the implementation very complicated.

  1. This is not correct, catch block can throw an exception without terminate being called, it is even explicitly supported via throw;.

  2. Depends on what you call "nested", I know 2 more cases leading to std::terminate but "nested" exceptions are supported as explained below.

    • Constructor of the exception object in the thrown expression throws.
    • Copy constructor in a catch block if captured by value throws.

In general, the exception handling is:

  1. throw statement is executed, the exception object E is created from the expression and stored somewhere, let's say "likely not on the stack".
  2. The matching catch block handler for the exception handler is found and the control is transferred to it. If such handler does not exist, std::terminate is called.
  3. During transferring of the control, relevant local objects are properly destroyed in the reverse order of their construction.
  4. The catch block's argument is constructed from E,the exception is now considered caught and the execution of catch block starts. Any throw inside the block is a new exception and the process begins anew.

The Standard does not specify the order of 2, 3 . Compilers can first search for the handler and if not found, terminate the program immediately, stack unwinding is not guaranteed to happen in this case. AFAIK gcc,clang do not unwind the stack in this case now.

Stack unwinding calls the destructors of local objects which can call further functions. Those functions can throw leading to more stack unwinding and more destructor calls which again can lead to more exceptions... This is a perfectly valid way how you can have any amount of pending, yet-unhandled exceptions active at the same time. You can actually query that amount by calling std​::​uncaught_­exceptions

The only issue is when the thrown uncaught exception escapes into the stack unwinding process of the previous (still) uncaught, pending exception - i.e. when it is thrown from a destructor directly called during by the stack unwinding algorithm because there wasn't any matching handler found inside. This is the case which leads to std::terminate call because there isn't much else you can reasonably do.

Throwing from a destructor in itself is supported and can be caught but since you do not know what exactly triggered that call so it is strongly discouraged, actually

if (std::uncaught_exceptions()==0)// Safe throw from dtor.
    throw 42;

will work but please pretend you did not read that. One should also ask themselves what it means if an object refused to be destroyed.

CodePudding user response:

The way I phrased my question above makes the answer seem, perhaps, not that surprising.

In particular, when I began researching this, I did not consider the case of an uncaught exception, as in point 1, above.

With this considered, it is perhaps unsurprising that the result is the same in both cases. terminate() is called.

Here is a link to a Godbolt demonstration. The Executor pane shows that a warning is produced at compile time, and that terminate is called. This information presumably comes back from the operating system.

On further investigation it appears that the terminate() function by default is setup to call abort() but that this can be changed.

abort() then appears to send a signal to the OS, SIGABRT, which on Linux systems has the value 134 or 6. I found conflicting information on this, I am not sure which is correct.

Here are some references which I will update in time.

https://komodor.com/learn/sigsegv-segmentation-faults-signal-11-exit-code-139/#:~:text=SIGSEGV (exit code 139) vs SIGABRT (exit code,violation, and may terminate it as a result.

https://www.man7.org/linux/man-pages/man7/signal.7.html

https://www.man7.org/linux/man-pages/man3/abort.3.html

I posted this link inline, but it also gives some details about the possible methods which can call terminate().

https://www.ibm.com/docs/en/zos/2.1.0?topic=functions-terminate-function

On my Linux test machine, which uses Linux kernel 5.10.0-18-amd64, abort() prints Aborted and echo $? prints 134.

  • Related