'Breaking on unhandled exceptions in other people's code

Note: this entire discussion is exclusively about unchecked exceptions. Checked exceptions have nothing to do with what I am talking about here.

So, I have my Intellij IDEA debugger configured to only break on unhandled exceptions.

Of course this would not work without some extra love, because language constructs such as try-with-resources catch and rethrow, thus causing the debugger to break not at the point where the exception was thrown, but instead at the point where the exception is rethrown, which is useless, but I have gone through the trouble of putting in all the necessary extra love (I will spare you from the details) and I have gotten things to work reasonably well.

So, when an exception is thrown anywhere within my code, I never have to guess what went wrong by poring through post-mortem stack traces in logs; I can see what went wrong by having the debugger stop right at the throw statement.

This all works reasonably well for the most part; specifically, it works reasonably well for as long as all the code involved is my code. unfortunately, sometimes I also have to deal with other people's code.

When I call Jim's function, which in turn calls my function, and my function throws, then quite often this exception is not treated as an unhandled exception, because Jim's function quite often contains a try-catch. When this happens, and depending on what Jim does in his try-catch statement, the debugger will either stop somewhere within Jim's code, or it will not stop at all, and there will be a stack trace in the log if I am lucky. In either case, my goal will not be met: the debugger will not stop on the throw statement.

For example, if I register an observer with Swing, and Swing invokes my observer, and my observer throws an exception which is unhandled as far as I am concerned, the exception will certainly not be unhandled as far as Swing is concerned, because Swing has a try-catch at the very least in the main loop of its Event Dispatcher Thread. So, the debugger will never break on the throw statement.

So, my question is:

Is there anything I can do to convince the debugger to stop on exceptions that are unhandled as far as I am concerned?

To put it in different terms: is there any way to let the debugger know what the boundaries of my code are, so that it can stop on exceptions that cross those boundaries?

Please note that I may not necessarily have freedom to change the throw statement: I may in turn be invoking yet a third library, which may be throwing the exception, or I may be invoking some code of mine which is general purpose, so its throw statement needs to stay as it is, because there probably exists some test which exercises that code to ensure that it throws the expected exception under the right circumstances.

I am using IntelliJ IDEA, if that matters.



Solution 1:[1]

So, a month later, here is how I solved this problem.

(Luckily, it did not involve any custom thread-pool.)

The DL;DR version:

  1. At each and every entry-point to your code, add a statement which redirects the flow of execution through a special-purpose "debug" class, which catches all exceptions thrown by your code and just rethrows them.
  2. Use the debugger's "Catch class filters" feature to specify that you want the debugger to stop not only on uncaught exceptions, but also on caught exceptions if they are caught within that special-purpose "debug" class.

There are some subtleties, so if you try doing this, be sure to also read the long version.

The long version:

Note that I am including a package name here as an example, because I need to refer to it further down. You can use any package name that suits you, as long as you are consistent.

Here is the special-purpose "debug" class:

package mikenakis.debug;

import java.util.function.Supplier;

public final class Debug
{
    public static boolean expectingException;

    private Debug()
    {
    }

    /**
     * Invokes a given {@link Runnable}, allowing the debugger to
     * stop at the throwing statement of any exception that is 
     * thrown by the {@link Runnable}, even if the caller of this
     * method has a catch-all clause.
     *
     * For this to work, the debugger must be configured to stop
     * not only on uncaught exceptions, but also on caught
     * exceptions if (and only if) they are caught within this
     * class.
     *
     * @param procedure0 the {@link Runnable} to invoke.
     */
    public static void boundary( Runnable procedure0 )
    {
        if( expectingException )
        {
            procedure0.run();
            return;
        }
        //noinspection CaughtExceptionImmediatelyRethrown
        try
        {
            procedure0.run();
        }
        catch( Throwable throwable )
        {
            throw throwable;
        }
    }

    /**
     * Invokes a given {@link Supplier} and returns the result,
     * allowing the debugger to stop at the throwing statement of
     * any exception that is thrown by the {@link Supplier}, even
     * if the caller of this method has a catch-all clause.
     *
     * For this to work, the debugger must be configured to stop
     * not only on uncaught exceptions, but also on caught
     * exceptions if (and only if) they are caught within this
     * class.
     *
     * @param function0 the {@link Supplier} to invoke.
     * @param <T>       the type of the result returned by the
     *                  {@link Supplier}.
     *
     * @return whatever the {@link Supplier} returned.
     */
    public static <T> T boundary( Supplier<T> function0 )
    {
        if( expectingException )
            return function0.get();
        //noinspection CaughtExceptionImmediatelyRethrown
        try
        {
            return function0.get();
        }
        catch( Throwable throwable )
        {
            throw throwable;
        }
    }
}

Note: the expectingException flag is used by testing code that tests that certain methods-under-test throw certain exceptions under certain circumstances. Clearly, in such a scenario we want the exception thrown by the method-under-test to propagate to the test method without the debugger stopping, so this flag is used to temporarily disable the functionality of the Debug class.

Here is how to configure the IntelliJ IDEA debugger to stop on caught exceptions that are caught by the Debug class:

  • Go to "Run" -> "View Breakpoints..."
    • Under "Java Exception Breakpoints":
      • You should already have one entry for "Any exception" where "Caught exception" is unchecked and "Uncaught exception" is checked so that your debugger always stops on any uncaught exception thrown anywhere. We will now add one more exception breakpoint so that your debugger can stop on caught exceptions that are caught by any method in the Debug class I showed above.
      • Add a Java Exception Breakpoint.
      • Set the exception class to be java.lang.Throwable.
        • PEARL: In the "Search by name" tab of the "Enter Exception Class" dialog, if you check the 'Include non-project items' box and start typing 'Throwable' or 'java.lang.Throwable', IntelliJ IDEA will fail/refuse to find Throwable, presumably because it is covered by the 'Any exception' entry which they are already providing, and they cannot imagine why one would need to add one more entry for the same exception class. Luckily, their mechanism of preventing you from finding Throwable is half-assed, so it can be circumvented, as follows:
          • Switch to the 'Project' tab instead of the 'Search by Name' tab.
          • Navigate to 'External Libraries', then to your JDK, then to the java.base module, then to the java.lang package
          • Locate Throwable and select it.
      • Ensure "Caught exception" is checked.
      • Check the checkbox of "Catch class filters:" and enter mikenakis.debug.Debug.
      • PEARL: The "Breakpoints" dialog of IntelliJ IDEA contains many text boxes expecting class names, and in any of these text boxes you can enter any random garbage, and IntelliJ IDEA will happily accept it, without the slightest hint that it is wrong. Then, when you expect it to break, it will silently fail to break. To avoid this, never type a class name in any of these text boxes, always use the navigation feature to locate a class which is guaranteed to exist. (And good luck if you ever decide to rename or move that class.)

Here is what you have to do at each entry-point to your code:

This is an example of a piece of code that you might write which is invoked from external code. This particular example is a WindowListener that you would register with AWT/Swing, and AWT/Swing would then be invoking it and catching any exception thrown by it. Note how we force the flow of execution to pass through the Debug class.

private final WindowListener windowListener = new WindowAdapter()
    {
        @Override public void windowActivated( WindowEvent e )
        {
            Debug.boundary( () -> onWindowActivationStateChanged( true ) );
        }
        @Override public void windowDeactivated( WindowEvent e )
        {
            Debug.boundary( () -> onWindowActivationStateChanged( false ) );
        }
    };

private void onWindowActivationStateChanged( boolean active )
{
    throw new RuntimeException( "Ooops, I had an accident!" );
}

If your onWindowActivationStateChanged() method inadvertently throws:

  • Without Debug.boundary():

    • You might not even become aware of the fact that it threw, unless you are keeping an eye on the log, which you are unlikely to be doing, since with AWT/Swing your attention is probably focused on the window of the GUI application you are developing, not on the logging panel of the IDE.
    • In any case, your debugger will not break on the throwing statement, which is precisely the goal of this entire ordeal.
  • With Debug.boundary() and the Intellij IDEA debugger configuration described above:

    • The debugger will break on the throwing statement.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1