⚡ Section 7 · Question #4

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 the java.lang.RuntimeException subtree.
  • If a method executes code that declares a checked exception, the developer must implement one of two options (the “Catch or Specify” rule):
    1. Enclose the statement in a try-catch block.
    2. Declare the exception in the method signature using the throws keyword.

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:

  1. Thread Termination: Abruptly crashes the worker thread.
  2. Information Leakage: Exposes sensitive internal stack traces and database schemas to HTTP clients.
  3. 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 @RestControllerAdvice intercepts unhandled exceptions, logs them with correlation IDs, and renders sanitized RFC 9457 ProblemDetail responses.
  • 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 as OutOfMemoryError, InternalError, UnknownError) signals an irrecoverable failure of the JVM runtime itself.
  • When OutOfMemoryError occurs, the Java Heap is exhausted, internal JVM data structures are corrupted, and synchronization monitors may be left unlocked or half-modified.
  • Catching Error turns 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 Error is 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:

  1. new Thread(runnable).start(): The thread crashes immediately. The stack trace prints to System.err, but the main application process may never record the failure in structured application log files.
  2. ExecutorService.execute(Runnable): The worker thread crashes and is destroyed. The pool instantiates a new replacement thread, and the exception prints to standard error.
  3. ExecutorService.submit(Callable): The exception is caught by the internal FutureTask wrapper and stored. If the invoking code never calls future.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(), or BlockingQueue.take(), an external call to thread.interrupt() cancels the blocking wait by throwing InterruptedException.
  • Crucial JVM Invariant: Throwing InterruptedException automatically clears the thread’s interrupted status flag back to false!

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 via throws.
  • 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.setDefaultUncaughtExceptionHandler to log unhandled thread exceptions and terminate pods on fatal VM errors.

Handling Responsibility Summary

  • Checked Exceptions: Catch locally or declare via throws (enforced by javac).
  • Unchecked Exceptions: Catch centrally at system boundaries via @RestControllerAdvice or 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 during OutOfMemoryError).
  • ❌ “The compiler forces developers to handle NullPointerException.” (NPE is unchecked; the compiler imposes zero handling rules on it).
  • ❌ “When InterruptedException occurs, 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).