Exception handling in ScheduledExecutorService

Java’s ScheduledExecutorService allows you to schedule Runnable tasks without having to worry too much about creating Threads. At its simplest, you can schedule a task like this:

Runnable task = () -> System.out.println("Hello world!");
executor.schedule(task, 10, TimeUnit.SECONDS);
System.out.println("Done!");

The output is:

Done!
Hello world!

That is we schedule the task, then print “Done!”. 10 seconds later the scheduled task executes and prints “Hello world!”.

But what happens if the Runnable throws an Exception?

Where does it go? Lets try it:

ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
Runnable task = () -> System.out.println("42 /  0 = " + 42/0);
try {
    executor.schedule(task, 0, TimeUnit.MILLISECONDS);
} catch (Exception e)  {
    e.printStackTrace();
}
System.out.println("Done!");

The result is:

Done!

The task is successfully scheduled and started but nothing handles the exception. Wrapping the schedule in a try/catch block doesn’t help because that’s not where the exception is. How do we handle this exception? We have a few options.

Option 1: Handle the result

The result of a scheduled task is available as a ScheduledFuture. A ScheduledFuture does not immediately contain the result. It can’t as the task is scheduled to run later. However it allows us to wait for the result with ScheduledFuture.get(). ScheduledFuture.get() returns the return value of a Callable (void return for Runnables) OR rethrows the Runnable / Callable exception if it failed. So we can handle exceptions like this:

ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
Runnable task = () -> System.out.println("42 /  0 = " + 42/0);
ScheduledFuture<?> result = executor.schedule(task, 10, TimeUnit.SECONDS);
try {
    result.get();
} catch (Exception e) {
    e.printStackTrace();
}

The code is a little messy and it will wait at line 5 for the Runnable to be scheduled and then complete. Waiting for the Runnable to complete is usually not desirable for this sort of asynchronous code.

Option 2: Catch the exception in the Runnable

A better solution is to ensure that the Runnable task never throws an exception. That is, wrap the Runnable implementation in a try / catch block. Doing this inline can be a little messy so I’ll often create a wrapper method to do this:

private static Runnable errorHandlingWrapper(Runnable action) {
    return () -> {
        try {
            action.run();
        } catch (Throwable e) {
            e.printStackTrace();
        }
    };
}

The errorHandlingWrapper can be used like this:

ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
Runnable task = errorHandlingWrapper(() -> System.out.println("42 /  0 = " + 42/0));
executor.schedule(task, 10, TimeUnit.SECONDS);

This code is not cluttered by exception handling and produces the result we want:

Done!
java.lang.ArithmeticException: / by zero

It also has the advantage that we don’t need the main Thread to wait on the scheduled Thread to complete.

Option 3: Handle uncaught exceptions in the ThreadPoolExecutor

Finally if you want to manage exception handling at an application-wide level, consider extending the ThreadPoolExecutor. ThreadPoolExecutor.afterExecute() allows you to build custom behaviour on termination of every execution by this executor. Read the Javadoc carefully for notes on how to handle exceptions. The exception thrown by the Runnable is not necessarily available in the Throwable argument to this method as you might expect. Instead, you need to inspect the Runnable for its exception status. It suggests an implementation like this:

class ExtendedExecutor extends ThreadPoolExecutor {
   // ...
   protected void afterExecute(Runnable r, Throwable t) {
     super.afterExecute(r, t);
     if (t == null && r instanceof Future<?>) {
       try {
         Object result = ((Future<?>) r).get();
       } catch (CancellationException ce) {
           t = ce;
       } catch (ExecutionException ee) {
           t = ee.getCause();
       } catch (InterruptedException ie) {
           Thread.currentThread().interrupt(); // ignore/reset
       }
     }
     if (t != null)
       System.out.println(t);
   }
 }

In the example above, we want to extend ScheduledThreadPoolExecutor with this behaviour giving us a class like this:

public static class LoggingScheduledThreadPoolExecutor extends ScheduledThreadPoolExecutor {
        public LoggingScheduledThreadPoolExecutor(int corePoolSize) {
            super(corePoolSize);
        }

        @Override
        protected void afterExecute(Runnable r, Throwable t) {
            super.afterExecute(r, t);
            // same as above implementation
        }
    }

You can invoke it like this:

ScheduledThreadPoolExecutor executor = new LoggingScheduledThreadPoolExecutor(1);
Runnable task = () -> System.out.println("42 /  0 = " + 42/0);
executor.schedule(task, 10, TimeUnit.SECONDS);
System.out.println("Done!");

Which is best?

I usually prefer Option 2. Option 1 is simplest but will cause the main thread to block while we await the result of the scheduled thread. If that’s the behaviour you want, no problem. But this often defeats the point of spawning a new thread to do work.

Option 3 feels a little hacky to me and I feel it moves the exception handling a little too far away from the code that can exception. In the example above, it’s not too clear when we schedule the work that the ScheduledThreadPool is handling exceptions for us. However it’s a neat trick if you’re happy to define exception handling logic as a system-wide concern.

Option 2 is just right, particularly if you extract the exception handling to a static method. The calling code remains clean and we’ve made the exception handling fairly obvious by wrapping our task in a handler.

Of course the worst option is to do nothing with this exception. If you don’t explicitly check the ScheduledFuture result and you allow your Runnable to throw an unhandled exception, it will simply be lost.

Leave a Reply

Your email address will not be published. Required fields are marked *