The API is.. dubiously designed here.
Thinking about it in the abstract, there is no meaningful difference between join() and get(). They both wait, and as they are primarily about 'wait for another thread to finish a job', and the JDK spec clearly specifies that the core operations that all such operations are built on top of are guaranteed interruptable (namely, Thread.sleep, obj.wait, that sort of thing), then so are both join() and get().
Hence, both operations do essentially the same thing and can run into the same extraneous situation (a thread interrupt), and yet, they handle it differently which is bad API design.
But, reading the javadoc in more detail, the docs explain it. Note that get() is inherited from Future whereas join() is not.
The docs show the OpenJDK team at odds with themselves: The API design of Future's get() leans into checked exceptions: It can throw both ExecutionException and InterruptedException (both checked). In contrast, CompletableFuture has a getNow method which has replaced ExecutionException with CompletionException which is a runtime exception, and the javadoc explicitly spell out that they rewrap all the checked stuff into unchecked stuff 'for convenience'. ^1
However, delving into the code, there is a large difference after all. One should not rely on as the javadoc do not guarantee this difference:
get() calls the underlying job infrastructure with the 'mayInterruptIfYouWant' set to 'true' whereas join()sets it to false. For example, if the flag is up at the moment you invokeget(), then get() will return __immediately__ by clearing the flag and throwing InterruptedException(that's correct behaviour; raising the flag / keeping it raised _and also_ throwing that exception is not right), whereasjoin()` will just wait and ignore the fact that the flag is up. Once the future value is calculated, it returns normally and doesn't mess with the flag in any way (which means it will remain raised).
That means in practice join() is less likely to end by throwing InterruptedException than get() would be. Instead, join() would finish normally, except.. the interrupted flag of your thread is up. Which means any call at any time to anything that waits is either guaranteed to (such as with Thread.sleep or obj.wait() which internally almost all 'wait for stuff' methods will end up invoking), or highly likely to (blocking reads/writes to I/O channels such as files or notwork ports) abort instantly with an exception and a lowering of that flag. That's a bad state to leave things in.
Armed with that information, we can describe the behaviour of the old code which is broken, and the new code which is broken in a different way. The right answer then, is neither of your 2 snippets.
Your old code
If interrupted, stops nearly instantly, raises the flag, and exits via exception. This is not what you want. In the end, an interrupt means what you want it to mean: It cannot occur unless you explicitly write code that interrupts, which means a catch (InterruptedEx) block should do.. whatever you want to happen when you wrote foo.interrupt() in that other source file. But when trying to generalize, that almost always means that you want to either throw an exception and keep the flag lowered, or you want to log some stuff or do some cleanup but not take responsibility for the interrupt and just keep going, leaving the actual dealing with it to other code, in which case you swallow the exception and keep on going. do not do both. And you're doing both, here. That's why it's probably broken.
Any thread whose interrupt flag is up is essentially unstable: The next call to Thread.sleep() / obj.wait() exits INSTANTLY, clearing the flag and throwing InterruptedException. And lots of code ends up invoking one of those when needing to wait. Any blocking I/O ops (such as reading from or writing to a file or network port, which also means any interaction with any database, microservices, you name it) usually also instantly exit with an exception and a lowering of the flag, on essentially all architectures: InputStream.read() isn't specced to throw InterruptedException solely so that a hypothetical OS that has uninterruptable I/O still can host a JVM, it'd a shame if that'd be the only reason a JVM cannot be ported to such an OS.
Hence, you shouldn't raise that flag 'just in case' or out of an abundance of caution: it actually makes your code more fragile, not less so. Unless, of course, you want this thread to explode within moments after finishing your code block, which you should want only if you haven't actually processed the interruption yet. That is not true for your first snippet: You have handled it, by throwing an exception. Hence, do not raise the flag.
Your new code
If interrupted, does not respond to that at all and just keeps waiting.
When your new code returns, the flag will still be up, which means that thread will likely explode soon after (about half of the JVM core libraries, if invoked, will result in an exception in one way or another), but it returns 'normally' (not with an exception.
Both are bad.
The correct code is likely:
try { CompletableFuture.allOf(someFuture, someOtherFuture).get(); } catch (InterruptedException | ExecutionException ex) { if (ex.getCause() instanceof CustomException customException) { throw customException; } throw new CustomException(CustomErrorCodeEnum.UNEXPECTED_ERROR, ex.getCause().getMessage()); }
i.e. the same thing you had but without the reraising of the flag.
We've improved things and made your code shorter, that's a win-win!
A note about interruptions
They do not happen unless you want it to.
Specifically, an interrupt flag being up / an InterruptedException being thrown cannot possibly occur unless you wrote code to make them happen. You must invoke interrupt() on a Thread instance, or invoke an API that does this, and the APIs that do so are very explicit about it. For example, someFuture.cancel(true) will interrupt a thread - that boolean parameter is literally called mayInterruptIfRunning for a good reason.
'interrupting' a JVM is a very different thing and does not result in InterruptedException!. For example, if you have a JVM running and you hit CTRL+C in its terminal window or use your OS activity monitor / task manager equivalent to 'kill' the JVM, then one of two things happen:
- You use a hard-kill option. The JVM. just. dies. Nothing is run, no shutdown hooks, no finalizers, no interruptedexceptions.
- You use a soft-kill option, such as CTRL+C. The JVM just dies in the sense that no Interruptedexceptions are thrown either and no finalization is run, but, any shutdown hooks are executed and can even stop the shutdown if they want to.
So, there are only 3 options:
You wrote t.interrupt() or future.cancel(true) or similar somewhere. In that case, you wrote that for a reason, and probably not "a cat walked over my keyboard and I liked the look of it". What was that reason? Whatever it was, put that in your catch block. There is no general answer to 'how do I deal with an InterruptedException' because there is no general answer to "why did you write t.interrupt(); what did you want to happen?". t.interrupt() is a signal: Do (something I want) but not here, in another thread. t.interrupt starts the process. You write the (something I want) in the catch (InterruptedException) block at the side of the thing you interrupted.
You did not write that anywhere. In that case the interrupt cannot possibly occur, there's no reason to worry about it, and your current code that throws a'UNEXPECTED_ERROR" is as good as anything; it's a defensive code: Code that cannot possibly run unless your understanding of your own system is broken (i.e. you have bugs). In that case you want an exception with loads of detail to bubble all the way up. That's vastly superior than a bug causing your code to silently do nothing or ignore the error.
You are writing a library; there is some other programmer that will use your stuff and thus, while you never wrote t.interrupt(), that programmer that uses your library might. This is the most complicated case. The best thing to do is to explicitly document (and then test!) what your library does when interrupts occur. In this case you would presumably expand upon CustomException to have a CustomErrorCodeEnum that reflects interruption. Or, much simpler, you document: You can interrupt the waiting for the completion of some futures; if you do that, it is dealt with the same way as if the calculation of the future value code throws something. You could instead document: "if you interrupt it, my library will attempt to stop waiting ASAP, will return some arbitrarily value, will not throw any exception, but will keep the flag raised". You can document that and test it, but that's unlikely to be behaviour that a user of your library wants.
join()can not be interrupted, it falls into the second category. So you don’t need to do anything yourself. But when you handleCompletionExceptionandCancellationExceptionwith the same code you can’t useex.getCause().getMessage()unconditionally. There is no guaranty that aCancellationExceptionhas a cause. Likewise, the first code snippet’s exception handler is questionable. You should not callThread.currentThread().interrupt()when catching anExecutionException..interrupt(). When you wrote that code, what did you think should happen? Put that in your catch block. Note that you're also raising that flag if ExecutionEx happens which is very bad.