11

I am using ExecutorService to send mails asynchronously, so there is a class:

class Mailer implements Runnable { ...

That handles the sending. Any exception that gets caught is logged, for (anonymized) example:

javax.mail.internet.AddressException: foo is bar
    at javax.mail.internet.InternetAddress.checkAddress(InternetAddress.java:1213) ~[mail.jar:1.4.5]
    at javax.mail.internet.InternetAddress.parse(InternetAddress.java:1091) ~[mail.jar:1.4.5]
    at javax.mail.internet.InternetAddress.parse(InternetAddress.java:633) ~[mail.jar:1.4.5]
    at javax.mail.internet.InternetAddress.parse(InternetAddress.java:610) ~[mail.jar:1.4.5]
    at mycompany.Mailer.sendMail(Mailer.java:107) [Mailer.class:?]
    at mycompany.Mailer.run(Mailer.java:88) [Mailer.class:?]
    ... suppressed 5 lines
    at java.lang.Thread.run(Thread.java:680) [?:1.6.0_35]

Not very helpful - I need to see the stacktrace that invoked the ExecutorService that caused all of this. My solution is to create an empty Exception and pass it into Mailer:

executorService.submit(new Mailer(foo, bar, new Exception()));
...
// constructor
public Mailer(foo, bar, Exception cause) { this.cause = cause; ...

And now in the case of exception I want to log the problem itself and its cause from the other thread:

try {
  // send the mail...
} catch (Throwable t) {
  LOG.error("Stuff went wrong", t);
  LOG.error("This guy invoked us", cause);
}

This works great but produces two logs. I want to combine t and cause into a single exception and log that one. In my opinion, t caused cause, so using cause.initCause(t) should be the right way. And works. I see a full stack trace: from where the call originated all the way up to the AddressException.

Problem is, initCause() works only once and then crashes. Question 1: can I clone Exception? I'd clone cause and init it with t every time.

I tried t.initCause(cause), but that crashes right away.

Question 2: is there another smart way to combine these 2 exceptions? Or just keep one thread context in the other thread context for logging purposes?

vektor
  • 3,312
  • 8
  • 41
  • 71
  • 2
    How about just getting the stack trace from the exception and working with that? – RealSkeptic Jun 08 '15 at 12:09
  • I want to leverage the logging mechanisms in Log4j as much as possible. Manipulating stack traces seems to be a hack that might break something. – vektor Jun 08 '15 at 12:10
  • "Problem is, initCause() works only once and then crashes." - how does it crash? – user3707125 Jun 08 '15 at 12:11
  • `java.lang.IllegalStateException: Can't overwrite cause at java.lang.Throwable.initCause(Throwable.java:320)` Simply put, empty `Exception` has cause equal to itself, once you set it to anything else, it will not allow replacing the cause. – vektor Jun 08 '15 at 12:15
  • I think that you'll have better luck setting the stack trace in the exception. What do you think that might break? – RealSkeptic Jun 08 '15 at 12:17
  • 1
    @RealSkeptic so I tried this `t.setStackTrace(ArrayUtils.addAll(t.getStackTrace(), cause.getStackTrace()));` and it seems to work. It's not too nice, for example I'd appreciate seeing the thread boundary somehow highlighted ("Caused by:" would be ideal), but it works. Write this up as an answer so that I can accept it :) – vektor Jun 08 '15 at 12:23
  • 1
    *"can I clone `Exception`?"* You can, by serializing it and immediately deserializing it. It's clunky but it works. – Radiodef Jun 08 '15 at 12:24
  • @Radiodef +1 but too much of a hack :) – vektor Jun 08 '15 at 12:26
  • 3
    @vektor, You can use `Exception e = new Exception(t); e.setStackTrace(cause.getStackTrace()); LOG.error("Stuff went wrong", e);` This way you should have a separate cause. – ivant Jun 08 '15 at 12:32
  • @ivant and we have a winner, that's precisely what I needed! How come `e.setStackTrace()` does not rewrite the stack trace from `t`? Or, are _cause_ and _stack trace_ completely separate fields of `Throwable`? – vektor Jun 08 '15 at 12:37
  • 2
    They are separate fields. And the javadoc for setStackTrace explains why it's allowed: https://docs.oracle.com/javase/7/docs/api/java/lang/Throwable.html#setStackTrace(java.lang.StackTraceElement[]) – ivant Jun 08 '15 at 12:40
  • Thanks alot @ivant, alas RealSkeptic has already written up the answer in this spirit, I am going to accept it. Thanks everyone for your help! – vektor Jun 08 '15 at 12:42
  • 1
    See [this](https://stackoverflow.com/q/49269620/14731) related question. There is an answer there that does what you want, but I'm actively researching an even better solution. – Gili May 02 '18 at 20:55

5 Answers5

9

Following my comment, this is actually what I had in mind. Mind you, I don't have a way to test it at the moment.

What you pass from your parent thread is New Exception().getStackTrace(). Or better yet, as @Radiodef commented, Thread.currentThread().getStackTrace(). So it's basically a StackTraceElement[] array.

Now, you can have something like:

public class CrossThreadException extends Exception {

    public CrossThreadException( Throwable cause, StackTraceElement[] originalStackTrace ) {

        // No message, given cause, no supression, stack trace writable
        super( null, cause, false, true );

        setStackTrace( originalStackTrace );
    }
}

Now in your catch clause you can do something like:

catch ( Throwable cause ) {
   LOG( "This happened", new CrossThreadException( cause, originalStackTrace ) );
}

Which will give you a boundary between the two stack traces.

RealSkeptic
  • 33,993
  • 7
  • 53
  • 79
  • 1
    You could also pass `Thread.currentThread().getStackTrace()` instead of `new Exception().getStackTrace()`. – Radiodef Jun 08 '15 at 12:40
  • @Radiodef Thank you, added that to the answer. – RealSkeptic Jun 08 '15 at 12:44
  • You don't even have to pass this as an argument. Since the Constructor is invoked on the parent thread, you can just set the stacktrace field in the constructor: `private StackTraceElement[] originalStackTrace = Thread.currentThread().getStackTrace();` right there as field initializer – Falco Jun 08 '15 at 15:33
3

You can use the Future<v> object that returned from your submit invocation, then invoke the get() method, if any exception occured during the task execution it will be re thrown.

Another option is to customize the default exception handler for the thread factory that creates the threads for your ExecutorService. See for more details: Thread.UncaughtExceptionHandler

Maxim Kirilov
  • 2,639
  • 24
  • 49
1

While I found @RealSkeptic's answer useful, I don't like the output. I think it's "upside-down", so here's my solution, that gives you the following output:

  1. exception in child thread
  2. cause in child thread
  3. "cause" in parent thread (including thread name)
com.example.MyException: foo is bar
    at com.example.foo.Bar(Bar.java:23)
    ...
Caused by: java.net.UnknownHostException: unknown.example.com
    at com.example.net.Client(Client.java:123)
    ...
Caused by: com.example.CrossThreadException: Thread: main
    com.example.thread.MyExecutor.execute(MyExecutor.java:321)
    ...
public class CrossThreadException extends Exception {
    public CrossThreadException(CrossThreadException parentThreadException) {
        this(null, null, parentThreadException);
    }

    public CrossThreadException(Throwable currentThreadCause, String skipPackage, CrossThreadException parentThreadException) {
        // No message, given cause, no supression, stack trace writable
        super(getMessageByCurrentThreadCause(currentThreadCause), parentThreadException);
        if (currentThreadCause != null) {
            if (skipPackage != null) {
                final StackTraceElement[] stackTrace = currentThreadCause.getStackTrace();
                Pair<Integer, Integer> startEnd = StackTraceHelper.stackTraceStartEnd(stackTrace, skipPackage, 0);
                setStackTrace(Arrays.copyOfRange(stackTrace, startEnd.getFirst(), startEnd.getSecond()));
            } else {
                setStackTrace(currentThreadCause.getStackTrace());
            }
        }
    }

    private static String getMessageByCurrentThreadCause(Throwable currentThreadCause) {
        return currentThreadCause != null
                ? String.format("Thread: %s - %s: %s", Thread.currentThread().getName(), currentThreadCause.getClass().getSimpleName(), currentThreadCause.getMessage())
                : String.format("Thread: %s", Thread.currentThread().getName());
    }
}

class ThreadHelper {
    public static final ThreadLocal<CrossThreadException> parentThreadException = new ThreadLocal<>();

    public static CrossThreadException getParentThreadException() {
        return parentThreadException = parentThreadException.get();
    }

    public static Throwable getCrossThreadException(Throwable cause) {
        CrossThreadException parentThreadException = getParentThreadException();
        if (parentThreadException == null) {
            return cause;
        }

        Throwable c = cause;
        while (c.getCause() != null && c.getCause() != c) {
            c = c.getCause();
        }
        c.initCause(parentThreadException);
        return cause;
    }
}

class MyExecutor extends ThreadPoolExecutor {
    @Override
    public void execute(Runnable command) {
        CrossThreadException parentThreadException = new CrossThreadException(ThreadHelper.getParentThreadException());
        super.execute(wrap(command, parentThreadException));
    }

    public static Runnable wrap(final Runnable runnable, final CrossThreadException parentThreadException) {
        return () -> {
            try {
                ThreadHelper.parentThreadException.set(parentThreadException);
                runnable.run();
            } finally {
                ThreadHelper.parentThreadException.set(null);
            }
        };
    }
}

usage: when using MyExecutor then you can catch and log an exception in the child thread:

try {
    ...
} catch (Throwable t) {
    log.error("Caught an exception", ThreadHelper.getCrossThreadException(t))
}
Gavriel
  • 18,880
  • 12
  • 68
  • 105
0

What I tend to do is have the Thread you're calling save the exception rather than throw it.

Then the main thread that started it can poll it to see if an exception occurred. If one has, then it can get the exception and throw a one with it as the cause.

I don't think you can clone an exception.

As a general rule, this sort of tinkering with Exceptions is bad news and a sign that there may be some problems with the general approach to error handling in the code.

It's hard to do, becuase you're not really meant to be doing it.

Phil Anderson
  • 3,146
  • 13
  • 24
  • I believe my problem is quite general: follow the stacktrace across thread boundaries. Is there a well-established solution to this? – vektor Jun 08 '15 at 12:21
  • I agree. The problem is entirely legitimate, and quite common. My approach is one way that I've used before to solve it, and is a well-established approach. I'm sure there are others, but my strong opinion is that trying to solve it by hacking about with the exceptions is a bad code-smell. – Phil Anderson Jun 08 '15 at 12:45
0

You can use the ListableFuture class from the Google Guava library. See https://code.google.com/p/guava-libraries/wiki/ListenableFutureExplained

ListeningExecutorService service = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(10));
ListenableFuture<Explosion> explosion = service.submit(new Callable<Explosion>() {
  public Explosion call() {
    return pushBigRedButton();
  }
});
Futures.addCallback(explosion, new FutureCallback<Explosion>() {
  // we want this handler to run immediately after we push the big red button!
  public void onSuccess(Explosion explosion) {
    walkAwayFrom(explosion);
  }
  public void onFailure(Throwable thrown) {
    battleArchNemesis(); // escaped the explosion!
  }
});
Wim Deblauwe
  • 25,113
  • 20
  • 133
  • 211
  • I still need to log `thrown` in `onFailure` in a way that preserves both stacks, how do I do that? – vektor Jun 08 '15 at 12:33