Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwingWorker exceptions lost even when using wrapper classes

I've been struggling with the usability problem of SwingWorker eating any exceptions thrown in the background task, for example, described on this SO thread. That thread gives a nice description of the problem, but doesn't discuss recovering the original exception.

The applet I've been handed needs to propagate the exception upwards. But I haven't been able to even catch it. I'm using the SimpleSwingWorker wrapper class from this blog entry specifically to try and address this issue. It's a fairly small class but I'll repost it at the end here just for reference.

The calling code looks broadly like

try {
    // lots of code here to prepare data, finishing with
    SpecialDataHelper helper = new SpecialDataHelper(...stuff...);
    helper.execute();  // this will call get+done on the actual worker
} catch (Throwable e) {
    // used "Throwable" here in desperation to try and get
    // anything at all to match, including unchecked exceptions
    //
    // no luck, this code is never ever used :-(
}

The wrappers:

class SpecialDataHelper extends SimpleSwingWorker {
    public SpecialDataHelper (SpecialData sd) {
        this.stuff = etc etc etc;
    }
    public Void doInBackground() throws Exception {
        OurCodeThatThrowsACheckedException(this.stuff);
        return null;
    }
    protected void done() {
        // called only when successful
        // never reached if there's an error
    }
}

The feature of SimpleSwingWorker is that the actual SwingWorker's done()/get() methods are automatically called. This, in theory, rethrows any exceptions that happened in the background. In practice, nothing is ever caught, and I don't even know why.

The SimpleSwingWorker class, for reference, and with nothing elided for brevity:

import java.util.concurrent.ExecutionException;
import javax.swing.SwingWorker;

/**
 * A drop-in replacement for SwingWorker<Void,Void> but will not silently
 * swallow exceptions during background execution.
 *
 * Taken from http://jonathangiles.net/blog/?p=341 with thanks.
 */
public abstract class SimpleSwingWorker {
    private final SwingWorker<Void,Void> worker =
        new SwingWorker<Void,Void>() {
            @Override
            protected Void doInBackground() throws Exception {
                SimpleSwingWorker.this.doInBackground();
                return null;
            }

            @Override
            protected void done() {
                // Exceptions are lost unless get() is called on the
                // originating thread.  We do so here.
                try {
                    get();
                } catch (final InterruptedException ex) {
                    throw new RuntimeException(ex);
                } catch (final ExecutionException ex) {
                    throw new RuntimeException(ex.getCause());
                }
                SimpleSwingWorker.this.done();
            }
    };

    public SimpleSwingWorker() {}

    protected abstract Void doInBackground() throws Exception;
    protected abstract void done();

    public void execute() {
        worker.execute();
    }
}
like image 730
Ti Strga Avatar asked Dec 18 '12 20:12

Ti Strga


2 Answers

Forget about your wrapper, which eats the exceptions, whereas the SwingWorker doesn't. Here's how a SwingWorker should be used, and how you should handle a specific exception thrown from the background task:

class MeaningOfLifeFinder extends SwingWorker<String, Object> {
    @Override
    public String doInBackground() throws SomeException {
        return findTheMeaningOfLife();
    }

    @Override
    protected void done() { // called in the EDT. You can update the GUI here, show error dialogs, etc.
        try { 
            String meaningOfLife = get(); // this line can throw InterruptedException or ExecutionException
            label.setText(meaningOfLife);
        } 
        catch (ExecutionException e) {
            Throwable cause = e.getCause(); // if SomeException was thrown by the background task, it's wrapped into the ExecutionException
            if (cause instanceof SomeException) {
                // TODO handle SomeException as you want to
            }
            else { // the wrapped throwable is a runtime exception or an error
                // TODO handle any other exception as you want to
            }
        }
        catch (InterruptedException ie) {
            // TODO handle the case where the background task was interrupted as you want to
        }
    }
}
like image 74
JB Nizet Avatar answered Nov 15 '22 19:11

JB Nizet


The wrapper seems to works as expected. However, its implementation will never call done() if exception occurs. This is not suitable for many cases. It's probably simpler to call get() in done(). This will throw whatever exception that happened in doInBackground().

Not sure how your example is structured, but it did not work in the application without EDT. So wrapping worker execution in SwingUtilities.invokeLater did help, ie:

SwingUtilities.invokeLater(new Runnable() {
    public void run() {
        new SpecialDataHelper().execute();
    }
});

The following example does print the exception stack trace:

public class Tester {

    static class SpecialDataHelper extends SimpleSwingWorker {
        public SpecialDataHelper () {
        }
        public Void doInBackground() throws Exception {
            throw new Exception("test");
        }
        protected void done() {
        }
    }

    public static void main(String[] args) {
        try{
            SwingUtilities.invokeLater(new Runnable() {
                public void run() {
                    new SpecialDataHelper().execute();
                }
            });
        } catch(Exception ex){
            ex.printStackTrace();
        }
    }
}

Also consider this simple example that demonstrates how to get the exceptions that occurred in doInBackground() without using the wrapper. The wrapper is just a helper in case you forget to call get().

import javax.swing.JOptionPane;
import javax.swing.SwingUtilities;
import javax.swing.SwingWorker;

public class Tester {
    static class Worker extends SwingWorker<Void,Void> {
        @Override
        protected Void doInBackground() throws Exception {
            throw new Exception("test");
        }
        @Override
        protected void done() {
            try {
                get();
                JOptionPane.showMessageDialog(null, "Operation completed");
            } catch (Exception ex) {
                JOptionPane.showMessageDialog(null, "Operation failed");
            } 
        }
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                new Worker().execute();
            }
        });         
    }
}
like image 20
tenorsax Avatar answered Nov 15 '22 20:11

tenorsax