Which exceptions must be handled
The answer depends on whether "mandatory" is defined by the Java compiler (javac) or by application reliability architecture.
🟢 Junior Level
The answer depends on whether “mandatory” is defined by the Java compiler (javac) or by application reliability architecture.
1. From the Compiler (javac) Perspective
Only Checked Exceptions are strictly mandatory to handle at compile time:
- These are all classes inheriting from
java.lang.Exception, excluding thejava.lang.RuntimeExceptionsubtree. - If a method executes code that declares a checked exception, the developer must implement one of two options (the “Catch or Specify” rule):
- Enclose the statement in a
try-catchblock. - Declare the exception in the method signature using the
throwskeyword.
- Enclose the statement in a
If neither option is satisfied, the code fails to compile:
// COMPILATION ERROR: Unhandled exception: java.io.IOException
public void readConfiguration() {
byte[] data = Files.readAllBytes(Path.of("config.json"));
}
2. Comprehensive Handling Requirement Matrix
| Exception Category | Representative Examples | Mandatory by Compiler? | Mandatory in Architecture? |
|---|---|---|---|
| Checked Exceptions | IOException, SQLException, InterruptedException |
✅ Yes (Strict) | ✅ Yes (Local handling or translation) |
| Unchecked Exceptions | NullPointerException, IllegalArgumentException |
❌ No | 🟡 Yes (At system boundaries & thread perimeters) |
| JVM Errors | OutOfMemoryError, StackOverflowError |
❌ No | 🛑 Forbidden to catch (Allow JVM to fail fast) |
🟡 Middle Level
Mandatory Handling at System Boundaries
Although the compiler does not enforce try-catch blocks for RuntimeException, an uncaught unchecked exception in an enterprise application causes severe damage:
- Thread Termination: Abruptly crashes the worker thread.
- Information Leakage: Exposes sensitive internal stack traces and database schemas to HTTP clients.
- Poison Pills: Stalls distributed message consumers (Kafka, RabbitMQ) by repeatedly reprocessing broken messages in an infinite loop.
For these reasons, handling unchecked exceptions is architecturally mandatory at the system perimeter:
- REST API Boundaries: A centralized
@RestControllerAdviceintercepts unhandled exceptions, logs them with correlation IDs, and renders sanitized RFC 9457ProblemDetailresponses. - Message Broker Consumers: Kafka/RabbitMQ consumer loops catch deserialization and business failures, routing unprocessable payloads to a Dead Letter Queue (DLQ) to prevent stream stalling.
Why Catching Error or Throwable Is Dangerous
Beginner developers sometimes attempt to make applications “bulletproof” by catching everything:
// ❌ DANGEROUS ANTI-PATTERN:
try {
processBatchData();
} catch (Throwable t) { // Catches Error, OutOfMemoryError, ThreadDeath!
log.error("Something went wrong, continuing...", t);
}
Why This Is Fatal:
java.lang.Error(such asOutOfMemoryError,InternalError,UnknownError) signals an irrecoverable failure of the JVM runtime itself.- When
OutOfMemoryErroroccurs, the Java Heap is exhausted, internal JVM data structures are corrupted, and synchronization monitors may be left unlocked or half-modified. - Catching
Errorturns the service into a “zombie process”: threads hang indefinitely, database locks remain held, and the container fails to answer health probes. - The only correct architectural action upon encountering an
Erroris to let the JVM crash immediately, allowing Kubernetes or the system supervisor to restart the pod cleanly.
🔴 Senior Level
Uncaught Exceptions Across Concurrency Boundaries
When an unchecked exception escapes the execution stack of an asynchronous worker thread, the outcome depends strictly on how the thread was spawned:
new Thread(runnable).start(): The thread crashes immediately. The stack trace prints toSystem.err, but the main application process may never record the failure in structured application log files.ExecutorService.execute(Runnable): The worker thread crashes and is destroyed. The pool instantiates a new replacement thread, and the exception prints to standard error.ExecutorService.submit(Callable): The exception is caught by the internalFutureTaskwrapper and stored. If the invoking code never callsfuture.get(), the exception disappears completely without a trace!
Production Solution: Registering an UncaughtExceptionHandler
To guarantee that no asynchronous failure goes unrecorded, enterprise applications register a global fallback handler at startup:
package com.example.init;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
@Component
public class GlobalExceptionHandlerInitializer implements CommandLineRunner {
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandlerInitializer.class);
@Override
public void run(String... args) {
Thread.setDefaultUncaughtExceptionHandler((thread, throwable) -> {
if (throwable instanceof VirtualMachineError) {
log.error("FATAL: JVM VirtualMachineError in thread [{}]. Initiating container exit.",
thread.getName(), throwable);
// Fail-fast container shutdown for orchestrators (Kubernetes)
System.exit(1);
} else {
log.error("CRITICAL: Uncaught exception in thread [{}]",
thread.getName(), throwable);
}
});
}
}
The Cooperative Threading Contract of InterruptedException
InterruptedException is a checked exception that enforces Java’s cooperative threading model.
Threads in Java cannot be safely killed from the outside (the legacy Thread.stop() method was deprecated and permanently removed due to thread-safety violations). Stopping a thread is strictly cooperative:
- When a thread is blocked in
Thread.sleep(),Object.wait(), orBlockingQueue.take(), an external call tothread.interrupt()cancels the blocking wait by throwingInterruptedException. - Crucial JVM Invariant: Throwing
InterruptedExceptionautomatically clears the thread’s interrupted status flag back tofalse!
The Required Handling Protocol
If you catch InterruptedException, you must never swallow it. You must either rethrow it or restore the interrupted status:
public void runWorkerLoop() {
while (!Thread.currentThread().isInterrupted()) {
try {
Task task = taskQueue.take(); // Throws InterruptedException
task.execute();
} catch (InterruptedException ex) {
// MANDATORY: Restore interrupted status flag for the current thread!
Thread.currentThread().interrupt();
log.info("Worker thread received interruption request. Exiting loop cleanly...");
break; // Exit worker loop gracefully
}
}
}
4 Tricky Questions
1. What happens if a task submitted to executorService.submit(runnable) throws an ArithmeticException (division by zero)?
Answer:
The thread pool’s FutureTask intercepts and stores the ArithmeticException internally. No stack trace is written to the console or log files. If the caller never invokes future.get(), the exception is silently swallowed forever. If the caller subsequently calls future.get(), the original exception is rethrown wrapped inside a checked java.util.concurrent.ExecutionException.
2. Why is catching catch (Throwable t) considered a dangerous code smell in production code?
Answer:
Because Throwable is the superclass of both Exception and Error. Catching Throwable intercepts fatal JVM errors such as OutOfMemoryError or StackOverflowError. Instead of letting the JVM fail fast so container orchestrators (Kubernetes) can restart the broken pod, the application continues running in an unstable, memory-corrupted “zombie” state with corrupted locks and half-allocated objects.
3. What happens to the thread’s interrupted status flag when InterruptedException is thrown, and how must it be handled?
Answer:
The JVM automatically resets the thread’s interrupted status flag from true back to false when it throws InterruptedException. If a developer catches this exception and fails to call Thread.currentThread().interrupt(), the cancellation signal is permanently lost, preventing thread pools, executors, and schedulers from performing graceful shutdowns.
4. How does the javac compiler verify checked exception handling across multi-module dependencies at compile time?
Answer:
The compiler inspects the compiled .class bytecode metadata of dependencies. Methods that throw checked exceptions store those class names in the bytecode’s Exceptions attribute. The compiler traverses the Abstract Syntax Tree (AST) of the calling code and ensures that every method invocation with a non-empty Exceptions attribute is either enclosed in an appropriate try-catch or declared in the calling method’s own throws signature.
🎯 Interview Cheat Sheet
30-Second Summary
- Compiler Perspective: Only Checked Exceptions (
IOException,SQLException,InterruptedException) are mandatory to handle or declare viathrows.- Architectural Perspective: Unchecked Exceptions (
RuntimeException) are architecturally mandatory to catch at system boundaries (@RestControllerAdvice, Kafka Dead Letter Queues, asynchronous thread boundaries) to prevent thread death and information leakage.- Forbidden: Never catch
java.lang.Error(OutOfMemoryError). Fatal JVM errors require an immediate process crash so container orchestrators can restart the node cleanly.- Safety Net: Configure
Thread.setDefaultUncaughtExceptionHandlerto log unhandled thread exceptions and terminate pods on fatal VM errors.
Handling Responsibility Summary
- Checked Exceptions: Catch locally or declare via
throws(enforced byjavac). - Unchecked Exceptions: Catch centrally at system boundaries via
@RestControllerAdviceor DLQs. - InterruptedException: Always restore interrupt status via
Thread.currentThread().interrupt(). - Errors: Never catch; fail-fast and let Kubernetes reboot the container.
Red Flags (What to Avoid)
- ❌ “Wrap all top-level methods in
catch (Throwable t)so the application never crashes.” (Creates corrupted zombie processes duringOutOfMemoryError). - ❌ “The compiler forces developers to handle
NullPointerException.” (NPE is unchecked; the compiler imposes zero handling rules on it). - ❌ “When
InterruptedExceptionoccurs, log a warning and continue the loop.” (Clears the cancellation flag and prevents clean application shutdown). - ❌ “Checked exceptions should be exposed all the way to frontend REST clients.” (Infrastructure checked exceptions must be translated into domain exceptions at module boundaries).
Related Topics
- What is a checked exception and when to use it — When checked exceptions are appropriate
- What is an unchecked exception (Runtime Exception) — Unchecked exceptions in modern architecture
- What is the difference between Error and Exception — Fatal errors vs recoverable exceptions
- Why you should not swallow exceptions — The dangers of empty catch blocks
- What are suppressed exceptions — Suppressed exceptions in try-with-resources