Walk through correct exception handling and cooperative cancellation when working with Callable tasks and their Futures. What are the common mistakes?
answer
- ExecutionException wraps the task's throwable -> getCause()
- InterruptedException: restore flag (Thread.currentThread().interrupt())
- cancel(true) = interrupt request, cooperative only
- CPU loops must poll isInterrupted(); blocking calls throw on interrupt
- Never swallow InterruptedException in the task
basics
~20 sWrap get() to catch InterruptedException and ExecutionException, and read the real failure from getCause(). To stop a task, call cancel(true); the task only stops if its code checks the interrupt flag or calls interruptible methods. Always restore the interrupt flag.
solid answer
~40 sWhen you call future.get() you must handle InterruptedException (the waiting thread was interrupted — re-assert the interrupt with Thread.currentThread().interrupt() and usually bail out) and ExecutionException (the task threw — the original throwable is in getCause(), so log/handle that, not the wrapper). For a timed get(), also handle TimeoutException, remembering it doesn't cancel anything. Cancellation is cooperative: cancel(true) sets the worker's interrupt flag, but a task only halts if it periodically checks Thread.interrupted()/isInterrupted() or calls blocking methods that throw InterruptedException (sleep, wait, blocking queue ops). CPU-bound loops that swallow InterruptedException or never check the flag can't be stopped. Inside a Callable, never catch InterruptedException and silently continue — either propagate it or restore the flag. Also avoid letting a task swallow all exceptions, which hides failures from get().
go deeper
Knows get() can throw and that you must try/catch around it; aware cancel() exists.
Catches and distinguishes InterruptedException vs ExecutionException, unwraps getCause(), and adds TimeoutException for the timed get().
Explains cooperative cancellation end to end, the interrupt-flag restore idiom, why CPU loops must poll the flag, and the list of classic mistakes.
Defines an interruption/cancellation policy across a service, reasons about resource cleanup and corruption risks of forced stops, and propagates cancellation through chained tasks.
## Two independent failure axes With `Callable` + `Future` there are two separate concerns people conflate: 1. **The task failed** (its `call()` threw). 2. **The waiter was interrupted** (the thread calling `get()` got an interrupt). They surface as *different* exceptions and need *different* handling. ## Handling get() ```java try { Result r = future.get(); // or get(timeout, unit) use(r); } catch (ExecutionException e) { Throwable cause = e.getCause(); // the REAL failure from the task handleTaskFailure(cause); } catch (InterruptedException e) { Thread.currentThread().interrupt(); // restore the interrupt flag // typically abort: someone asked this thread to stop return; } catch (TimeoutException e) { // only for the timed get() future.cancel(true); // optional: stop the slow task } ``` ### Why unwrap ExecutionException? `Future.get()` has a fixed signature and can't declare every checked exception a task might throw. So the framework **wraps** whatever the task threw inside `ExecutionException`. The actual exception (and its stack trace) is in **`getCause()`**. Logging the `ExecutionException` itself, or its message, often loses the real diagnostic — always inspect `getCause()`. ### Why restore the interrupt flag? Catching `InterruptedException` **clears** the thread's interrupt status as a side effect. If you swallow it, callers higher up lose the signal that someone requested cancellation, and the thread won't notice it should stop. The idiom is `Thread.currentThread().interrupt();` (restore) and then return/propagate. This is the single most common interruption bug. ## Cooperative cancellation Java has **no safe way to forcibly kill a thread** (`Thread.stop()` is deprecated and dangerous because it can leave shared state corrupted). Instead, cancellation is **cooperative**: `future.cancel(true)` sets the worker thread's **interrupt flag** (and unblocks it from interruptible blocking calls). The task is only actually stopped if its code **cooperates**: ```java Future<List<Row>> f = pool.submit(() -> { List<Row> out = new ArrayList<>(); for (Row r : bigInput) { if (Thread.currentThread().isInterrupted()) { // check the flag throw new InterruptedException(); // or break/cleanup } out.add(transform(r)); // CPU-bound; must poll the flag itself } return out; }); ``` For blocking operations (`Thread.sleep`, `BlockingQueue.take`, `Object.wait`, many I/O calls), an interrupt makes them throw `InterruptedException` automatically — so those points are natural cancellation checkpoints. Pure CPU loops have **no** automatic checkpoint and must poll `isInterrupted()` themselves. After a successful `cancel`, `isCancelled()` and `isDone()` are true and `get()` throws `CancellationException`. ## The classic mistakes - **Swallowing InterruptedException** inside the task (`catch (InterruptedException e) {}`) — destroys the cancellation signal; the task becomes uncancellable. - **Not restoring the interrupt flag** when catching it in the waiter. - **Logging the ExecutionException wrapper** instead of `getCause()` — obscures the real error. - **Assuming cancel(true) kills the thread** — it only requests; non-cooperative code ignores it. - **Assuming TimeoutException cancels the task** — it doesn't; you must cancel explicitly. - **Catch-all `catch (Exception)` in the Callable** that returns a sentinel — hides the failure so `get()` never sees `ExecutionException`. ## Mental model Think of interruption as **a polite request, delivered by setting a flag**. The framework can deliver it; only your task code can honor it. Your job is (a) make tasks responsive to interruption, and (b) at the `get()` site, distinguish 'my task failed' (unwrap `getCause()`) from 'I was asked to stop' (restore the flag and bail).
- Why is Thread.stop() not an acceptable way to cancel a task instead of interruption?Thread.stop() is deprecated/unsafe: it asynchronously throws ThreadDeath at an arbitrary point, which can leave locks released and shared/mutable state half-updated and corrupted. Cooperative interruption lets the task stop at a safe point and clean up.
- What is the correct reaction to catching InterruptedException while calling future.get()?Restore the interrupt status with Thread.currentThread().interrupt() (since catching cleared it) and then abort the current operation, because something has asked this thread to stop.
saying these in an interview costs you the question
- Swallowing InterruptedException with an empty catch block.
- Forgetting to restore the interrupt flag after catching InterruptedException.
- Logging/handling the ExecutionException wrapper instead of getCause().
- Believing cancel(true) forcibly terminates the thread.
- Thinking a timed get()'s TimeoutException cancels the task.
- Using the deprecated Thread.stop() for cancellation.