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.
Be First to Comment