10

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();
    }
}
Community
  • 1
  • 1
Ti Strga
  • 1,353
  • 18
  • 40
  • 3
    All this is based on a false assumption. Read the [javadoc for the get() method](http://docs.oracle.com/javase/6/docs/api/javax/swing/SwingWorker.html#get%28%29): it throws an ExecutionException if the background computation threw an exception. – JB Nizet Dec 18 '12 at 21:02
  • See also this [Q&A](http://stackoverflow.com/q/7053865/230513). – trashgod Dec 18 '12 at 21:32
  • @JBNizet Yes, that's why SimpleSwingWorker's done() calls get(), catches ExecutionException, and rethrows it as a new RuntimeException. Isn't that the point? If not, then we're talking past each other and you'll have to be more explicit. – Ti Strga Dec 18 '12 at 21:44
  • @trashgod that thread has been edited so many times I can't follow the code :-( – Ti Strga Dec 18 '12 at 21:45
  • I don't understand, the [example](http://stackoverflow.com/a/7054627/230513) is complete. – trashgod Dec 18 '12 at 21:47
  • 1
    @TiStrga: the original SwingWorker forces you to handle an exception by calling `get()` in the overridden `done()` method, catching ExecutionException, and dealing with this exception. Your wrapper doesn't provide any way to handle the exception: either there is an exception, and it's thrown but nobody can catch it and the wrapper's `done()` method is never called, or there is no exception and the wrapper's `done()` method is called. The SwingWorker doesn't eat exceptions. Your wrapper does. – JB Nizet Dec 18 '12 at 21:53
  • I am so lost. Are we even still talking about the same wrapper? I thought SimpleSwingWorker was supposed to do exactly what you're talking about. Should I just delete this question and start over and leave out my code entirely? – Ti Strga Dec 18 '12 at 21:58
  • Sorry, @trashgod, I didn't realize you were talking about a specific answer in that thread, I was trying to read the starting post. I don't understand where I've gone wrong, I think I'm giving up on this project. Sorry to waste everyone's time. – Ti Strga Dec 18 '12 at 22:01
  • 1
    I'm talking about the SimpleSwingWorker class that you put in your question. SwingWorker is well designed. I suggest using it as explained in the javadoc. – JB Nizet Dec 18 '12 at 22:27
  • Then... what's the "false assumption"? The javadoc that you linked to talks about propertychangelisteners, which the rest of our code doesn't use as far as I know. I don't want to block while waiting for get(), I just want the eventual automatic call to done() to propagate exceptions. – Ti Strga Dec 18 '12 at 22:36
  • @TiStrga just call `get()` in `done()`. It will not block. This is actually what the wrapper does. The intention of the wrapper is to help in case you forget to call `get()`. Without calling `get()` you will not receive exceptions that occurred in `doInBackground()`. – tenorsax Dec 18 '12 at 22:43
  • You're starting your question by talking about the "well-known problem of SwingWorker eating any exceptions thrown in the background task". This is completely wrong. As shown in the javadoc of the `get()` method, which my link points to, any exception thrown by the background task will be wrapped in an ExecutionException that will be thrown when you call `get()` from the `done()` method that you must override. – JB Nizet Dec 18 '12 at 22:51
  • I'll rephrase it to be less offensive. Enough hits on Google and SO seemed to indicate that it's a common situation, but I guess we're talking about different problems. – Ti Strga Dec 18 '12 at 22:55
  • I have provided an answer showing how to get and handle an exception thrown by the background computation. – JB Nizet Dec 18 '12 at 23:02

3 Answers3

25

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
        }
    }
}
JB Nizet
  • 678,734
  • 91
  • 1,224
  • 1,255
  • I guess this is why I'm getting frustrated. `SimpleSwingWorker#done()` calls get, does the try-catch, and rethrows the exceptions. But you say it's eating them, and I don't understand how or why. I'm marking this answer as the correct one and then just deleting the code; there are probably lots of ways to get it right but none of my coworkers can tell me why this code doesn't work. – Ti Strga Dec 18 '12 at 23:11
  • 1
    +1 for generics `SwingWorker`, `SwingWorker is well designed.` -100 :-), I think that not, I'm disagree, I'm miss there methods implemented before SwingWorker was added to official Suns Api, – mKorbel Dec 19 '12 at 06:30
  • @TiStrga: the wrapper throws them, but nobody except the default exception handler of the EDT can catch them. So basically: you can't handle the thrown exceptions. – JB Nizet Dec 19 '12 at 08:02
  • @JBNizet I thought the done() method was also being called on the EDT, or at least, on the parent thread (which should be the same thing in this app), and (unchecked) exceptions thrown from there could be caught on the same thread. If that's not the case, then my understanding of SwingWorker is too flawed to ever repair. :-\ I've replaced all the live code; we'll just manage the threads directly. Thanks again for your patience! – Ti Strga Dec 20 '12 at 15:13
  • The done() method is called on the EDT. That's what I say in my code example. But it isn't called by you. It's called by the SwingWorker, just like an event listener: when the computation is finished, done() is called on the EDT. You must override done() and get the result of the task there. And getting the result of the task throws an ExecutionException if the background computation task threw an exception. – JB Nizet Dec 20 '12 at 15:16
  • Well, crap. :-( I was really hoping to be able to do code like the lop-most example, where a single try-catch fired off "execute()" and then caught the background exceptions. That was the goal of all the wrapper stuff, to grab ExecutionException and hand back the original cause to be handled at the same site. I guess we could move all the errorchecking down into done(), but not being able to re-throw out of done() is a bummer. – Ti Strga Dec 20 '12 at 17:16
3

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();
            }
        });         
    }
}
tenorsax
  • 21,123
  • 9
  • 60
  • 107
  • 2
    +1 for `invokeLater()`. Throwing an explicit exception, as you have shown, makes the effect easier to see. – trashgod Dec 18 '12 at 22:15
2

I think everyone is making this too complicated. Just try this:

String myResult="notSet";
try {
    // from your example above
    helper.execute();  // this will call get+done on the actual worker
    myResult=helper.get();
} catch (Exception e) {
// this section will be invoked if your swingworker dies, and print out why...
     System.out.println("exception ");
     e.printStackTrace() ;
     myResult="Exception "+e.getMessage();
}
return myResult;

The exceptions you throw but get eaten will be revealed. Two points will explain why this works. One, you only catch the remote exception from the calling thread whem you .get() the result. In more detail: To make the above example asynchronous, just move the .execute() higher up in the code. The moment you will discover the remote exception is after the asynch thread has bombed and you .get() the result. Two, by catching all Exceptions you will catch all special and subclassed exceptions that you may have built which the calling program may not have known about.

Jalkin
  • 1,132
  • 9
  • 6