ndm13
ndm13

Reputation: 1239

How does Java handle Errors in multithreading?

I'm working on a multithreading project that has a case where a Thread may throw an Error (not an Exception). Not finding any solid information about how Errors are handled in multithreading, I decided to do some tests and found that results can be inconsistent.

This is my test code, along with the commented results.

public class MultiThreadError {
    public static class ErrorThrowingRunnable implements Runnable{
        private final boolean throwsError;
        public ErrorThrowingRunnable(boolean throwsError){
            this.throwsError = throwsError;
        }

        @Override
        public void run() {
            try {
                // Wait between .5 and 1.5 seconds
                Thread.sleep(500 + new Random().nextInt(1000));
            } catch (InterruptedException ex) {}
            if(throwsError){
                throw new Error(Thread.currentThread().getName());
            }else{
                System.out.println(Thread.currentThread().getName());
            }
        }
    }

    public static void regularThreadPool(){
        // Crashes individual thread; swallows error
        ExecutorService threadPool = Executors.newFixedThreadPool(5);
        threadPool.submit(new ErrorThrowingRunnable(false));
        threadPool.submit(new ErrorThrowingRunnable(false));
        threadPool.submit(new ErrorThrowingRunnable(false));
        threadPool.submit(new ErrorThrowingRunnable(false));
        threadPool.submit(new ErrorThrowingRunnable(true));
        threadPool.shutdown();
    }

    public static void onDemandThreads(){
        // Crashes individual thread; displays error
        new Thread(new ErrorThrowingRunnable(false)).start();
        new Thread(new ErrorThrowingRunnable(false)).start();
        new Thread(new ErrorThrowingRunnable(false)).start();
        new Thread(new ErrorThrowingRunnable(false)).start();
        new Thread(new ErrorThrowingRunnable(true)).start();
    }

    public static void onDemandThreadPool(){
        // Same as onDemandThreads()
        ExecutorService threadPool = Executors.newFixedThreadPool(5);
        threadPool.execute(new ErrorThrowingRunnable(false));
        threadPool.execute(new ErrorThrowingRunnable(false));
        threadPool.execute(new ErrorThrowingRunnable(false));
        threadPool.execute(new ErrorThrowingRunnable(false));
        threadPool.execute(new ErrorThrowingRunnable(true));
        threadPool.shutdown();
    }

    public static void tooSmallThreadPool(){
        // When an error is thrown, apparently the thread that threw
        // the error is not reused, reducing the pool size
        ExecutorService threadPool = Executors.newFixedThreadPool(3);
        threadPool.execute(new ErrorThrowingRunnable(true));
        threadPool.execute(new ErrorThrowingRunnable(false));
        threadPool.execute(new ErrorThrowingRunnable(false));
        threadPool.execute(new ErrorThrowingRunnable(false));
        threadPool.execute(new ErrorThrowingRunnable(false));
        threadPool.execute(new ErrorThrowingRunnable(false));
        threadPool.execute(new ErrorThrowingRunnable(false));
        threadPool.execute(new ErrorThrowingRunnable(false));
        threadPool.shutdown();
    }
}

It seems as though the outcome is supposed to be what I expected: the thread throwing the Error terminates, displaying the message. It turns out that when a Runnable is passed to an ExecutorService using submit(Runnable), it's wrapped in a RunnableFuture<Void> that doesn't handle errors, and I can't find a way to change this behavior other than directly calling execute(Runnable), which for some reason doesn't exhibit the same behavior.

Is there a "best practice" for this? If I know a Thread may throw an Error, is there a way to submit it to an ExecutorService and not swallow the error?

Upvotes: 3

Views: 997

Answers (3)

MGorgon
MGorgon

Reputation: 2607

Yes, submit your task to ExecutorService and check the result in returned Future.

When used:

ExecutorService es = Executors.newFixedThreadPool(1);

Future<?> result = es.submit(new Runnable() {
    @Override
    public void run() {
        throw new Error("sample error");
    }
});

try {
    result.get();
} catch (ExecutionException e) {
    e.printStackTrace();
}

your stack trace will contain:

java.util.concurrent.ExecutionException: java.lang.Error: sample error
    at java.util.concurrent.FutureTask.report(Unknown Source)
    at java.util.concurrent.FutureTask.get(Unknown Source)
    at jjj.b.B.main(B.java:23)
Caused by: java.lang.Error: sample error
    at jjj.b.B$1.call(B.java:18)
    at jjj.b.B$1.call(B.java:1)
    at java.util.concurrent.FutureTask.run(Unknown Source)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
    at java.lang.Thread.run(Unknown Source)

Upvotes: 5

Joseph M. Dion
Joseph M. Dion

Reputation: 366

Trying to handle an Error thrown from a thread seems to be a suspicious use case...

From Java Doc : "An Error is a subclass of Throwable that indicates serious problems that a reasonable application should not try to catch. Most such errors are abnormal conditions. The ThreadDeath error, though a "normal" condition, is also a subclass of Error because most applications should not try to catch it."

The only time I needed to handle Error is when I use third parties that badly manage their exceptions, that throw Error rather than RuntimeException. In this specific cases, you need to catch the Error (or the Throwable) to avoid unexpected application crash.

Upvotes: 1

Joe C
Joe C

Reputation: 15714

A Future is an object that represents the result of running your operation, whether it be ready now or at some point in the future. Generally, one will call the get method on said future in order to get a result, particularly if you are passing a Callable rather than a Runnable into your ExecutorService.

If your Runnable/Callable throws an exception, this will be reflected in the Future object. You will be able to test whether the Runnable ran successfully by calling the get method. If it was a clean run, you will get (in this case) null back. If an exception was thrown, then get will throw an ExecutionException, with that exception marked as its cause.

Think of it like a coat check at a music venue. If they lose your coat, they probably won't tell you until you come with your ticket asking for your coat. The Future serves the purpose of your ticket here.

Upvotes: 0

Related Questions