Effective Exception Handling :: Swing Applications

The main difference between console and Swing applications is that the latter forces us to write code on the EDT, or potentially our own threads that are spawned from them. This forces our hand as far as exception handling is concerned, as neither the Runnable interface's run() method nor the various event handling methods allows us to propagate checked exceptions. This doesn't include the likes of ArithmeticException or NullPointerException, but affects others such as IOException or SQLException.

In my opinion, this is unfortunate. It would be preferable to allow exceptions to propagate further and be handled in a consistent manner by the JVM, such as displaying the error in a pop-up window. For the next example we will continue to elicit an ArithmeticException in the context of a Swing event handler.

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JButton;
import javax.swing.JFrame;

public class SwingTest extends JFrame {

    public static void main(String[] args) {
        new SwingTest().setVisible(true);
    }

    public SwingTest() {
        super("Swing Test");
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        JButton button = new JButton("Click me");
        button.addActionListener(new Listener());
        getContentPane().add(button);
        pack();
    }

    private class Listener implements ActionListener {
        public void actionPerformed(ActionEvent e) {
            int x = calculate();
        }
    }

    private int calculate() {
        return 1 / 0;
    }
}

1. Understand

The end result is not surprising - the calculation resulted in an error which is displayed on the console. I have trimmed the uninteresting stuff from the stack trace, but essentially what remains is this:

Exception in thread "AWT-EventQueue-0" java.lang.ArithmeticException: / by zero
        at SwingTest.calculate(SwingTest.java:28)
        at SwingTest.access$100(SwingTest.java:6)
        at SwingTest$Listener.actionPerformed(SwingTest.java:23)
        at javax.swing.AbstractButton.fireActionPerformed(AbstractButton.java:1995)
        ...
        at java.awt.EventDispatchThread.run(EventDispatchThread.java:122)

What may not be obvious is what happened to program flow. Our program did not perform a CTD or burst into flames. What did happen is that event dispatching thread was wound right back until the point where the initial button event was triggered. This is a good thing.

2. Evaluate

However, it did not provide any obvious information to the user unless they we also watching the console window at the time. This is not so helpful in a GUI environment where quite often the console is hidden away or not visible at all. We want to improve on this.

Now that we have an understanding of what has happened, can we do better? If the cause of the exception is bad data that is entered by the user, then obviously we should everything we can to sanitize that input. In this example it's a no-brainer to check if the user has supplied a zero as the divisor. But what can we do about conditions that are out of our control and can't adequately prepare for, such as loss of connectivity to an external resource?

3. Implement

What I propose we do is write a reusable handler class that displays errors in a dialog window. I'll try to use this in subsequent examples so it will need to work in GUI events as well as user defined threads. Otherwise it's quite simple.

import java.awt.EventQueue;
import java.io.PrintWriter;
import java.io.StringWriter;
import javax.swing.JOptionPane;

public class ExceptionHandler {

    public static void error(final Throwable error) {

        // do this first, in case all else fails
        error.printStackTrace();
		
        // only perform GUI operations in the EDT,
        // if in another thread then schedule it for the EDT
        if (EventQueue.isDispatchThread()) { 
            showError(error);
        } else {
            EventQueue.invokeLater(new Runnable() {
                public void run() {
                    showError(error);
                }
            });
        }
    }

    private static void showError(final Throwable error) {
	
        // convert stack trace to a single string
        StringWriter sw = new StringWriter();
        PrintWriter pw = new PrintWriter(sw);
        error.printStackTrace(pw);
		
        // display in a dialog window
        JOptionPane.showMessageDialog(null, sw, "Error", JOptionPane.ERROR_MESSAGE);
    }
}

We then try to catch exceptions as high up as possible and make use of the new handler:

private class Listener implements ActionListener {

    public void actionPerformed(ActionEvent e) {
        try {
            int x = calculate();
        } catch (Exception e) {
            ExceptionHandler.error(e);		
        }
    }
}

It doesn't look like much of a solution, does it? We're not even trying to handle different types of exception in their own unique way, or attempting to recover from them. Some might even be concerned about displaying a stack trace to the end user which reveals information about the source code.

My answer to this is we're only dealing with exceptions that we can't foresee. Most others we'll try to head off before they even happen, we can't do much to recover from those that remain.

Trying to soften the blow for the end user sounds like a good idea, but it often hampers our efforts to debug the problem. I'm quite happy to get a stack trace from an end user. In fact, one enhancement that could be made to the code above is to render the stack trace in a text area within a custom form rather than just a canned dialog. This makes it easier for the end users to copy and paste the entire error in text form, which is easier to handle than a screen dump image.

Additional Notes

Although the ExceptionHandler class works perfectly well in our own threads, there are a few enhancements we can make that go beyond what has been covered so far. I'll save that for the next chapter.