☕ Java
Error vs Exception
Java's throwable hierarchy has two branches under Throwable: Exception and Error. Exceptions represent conditions that a program can anticipate and potentially recover from. Errors represent serious conditions that arise in the Java runtime environment itself — out of memory, stack overflow, broken JVM — that are generally not recoverable at the application level. Understanding this distinction prevents the common mistake of catching Error subclasses in application code, which can produce unpredictable and dangerous behaviour.
The Throwable Hierarchy
Every object that can be thrown and caught in Java must extend java.lang.Throwable. Throwable has two direct subclasses: Exception and Error, and the entire exception and error system is built from these two branches.
Exception and its subclasses represent conditions that a reasonable application should anticipate and handle. They are further divided into checked exceptions (extending Exception directly) that the compiler requires to be handled or declared, and unchecked exceptions (extending RuntimeException) that the compiler allows to propagate silently. This branch of the hierarchy is the one application code works with day to day.
Error and its subclasses represent conditions from which the JVM itself cannot generally recover. They indicate fundamental failures in the runtime environment — the heap is exhausted, the call stack is overflowed, a required class cannot be loaded, or the JVM has detected internal corruption. These are not failures in the application's logic — they are failures in the infrastructure the application runs on. The application did not make a mistake (in most cases); the environment in which it runs has hit a hard limit or encountered a fatal condition.
The practical consequence of this design is that application code should almost never catch Error or its subclasses. When the JVM throws an OutOfMemoryError, it means there is literally no memory available to create new objects. Your catch block itself may fail because it requires memory. Even if it succeeds, the application is in an undefined state — some operation that needed memory was aborted mid-way, potentially leaving data structures in an inconsistent state. The correct response to most Errors is to log the condition if possible and let the application terminate, which the JVM's default uncaught exception handler does automatically.
Java
// ── Throwable hierarchy: ─────────────────────────────────────────────
//
// Throwable
// ├── Exception ← application-level failures
// │ ├── IOException ← checked (anticipated)
// │ ├── SQLException ← checked
// │ ├── ParseException ← checked
// │ └── RuntimeException ← unchecked (programming errors)
// │ ├── NullPointerException
// │ ├── IllegalArgumentException
// │ ├── ArrayIndexOutOfBoundsException
// │ └── ClassCastException
// └── Error ← JVM-level failures
// ├── OutOfMemoryError ← heap exhausted
// ├── StackOverflowError ← call stack full
// ├── VirtualMachineError ← JVM internal error
// │ ├── OutOfMemoryError
// │ └── InternalError
// ├── AssertionError ← assert statement failed
// ├── LinkageError ← class loading/linking failed
// │ ├── NoClassDefFoundError
// │ └── ClassCircularityError
// └── ThreadDeath ← deprecated thread stop mechanism
// ── Catching the hierarchy: ───────────────────────────────────────────
try {
// some code
} catch (RuntimeException e) {
// catches NullPointerException, IllegalArgumentException, etc.
} catch (Exception e) {
// catches checked exceptions: IOException, SQLException, etc.
} catch (Error e) {
// catches JVM errors — generally should NOT be done
} catch (Throwable t) {
// catches EVERYTHING — generally should NOT be done in app code
}Common Errors and Their Causes
Understanding the common Error subclasses and what causes them makes debugging faster and helps in writing code that prevents these conditions from occurring in the first place.
OutOfMemoryError is thrown when the JVM cannot allocate an object because the heap is full and garbage collection cannot free enough memory. Common causes are memory leaks (objects being held in collections or caches longer than their useful life), processing unreasonably large data sets in memory at once, or configuration with an insufficient heap size for the workload. The solution is usually profiling and fixing memory leaks, or processing data in smaller chunks. The JVM flags -Xmx (max heap) and -Xms (initial heap) control heap size.
StackOverflowError is thrown when the call stack is full. Every method call creates a stack frame; the stack has a finite size. The stack overflows when recursion is too deep — either unbounded recursion (no base case), mutually recursive methods with no termination, or legitimately deep recursion on very large data structures. The solution is to add or fix the base case, or convert the recursion to iteration using an explicit stack data structure.
AssertionError is thrown by the assert statement when assertions are enabled (JVM flag -ea). Unlike the others, AssertionError is thrown by application code rather than by the JVM itself. assert conditions are development-time checks that verify assumptions about program state — they should never be true in correct code, so their failure indicates a programming error. Assertions are disabled by default in production.
NoClassDefFoundError is thrown when the JVM was able to find a class at compile time (it was in the classpath) but cannot find it at runtime. This is a deployment issue — a jar file is missing from the runtime classpath, or the class was compiled against a version that is no longer present. It is distinct from ClassNotFoundException which is thrown by explicit Class.forName() calls when the class is simply not found.
Java
// ── OutOfMemoryError — heap exhaustion: ──────────────────────────────
try {
List<byte[]> leak = new ArrayList<>();
while (true) {
leak.add(new byte[1_000_000]); // allocate 1MB continuously
}
} catch (OutOfMemoryError e) {
System.err.println("Out of memory: " + e.getMessage());
// At this point, memory is critically low.
// The only safe action is to release large references and exit or
// let the process die. Attempting complex recovery is dangerous.
}
// Common causes:
// 1. Memory leak — objects held in static collections:
private static final List<Object> cache = new ArrayList<>();
// Items added to cache but never removed — grows forever
// 2. Reading entire large file into memory:
// byte[] data = Files.readAllBytes(Path.of("huge-file.dat")); // OOM for large files
// Better: process in chunks with streaming
// ── StackOverflowError — infinite recursion: ─────────────────────────
public static int badFactorial(int n) {
return n * badFactorial(n - 1); // no base case — infinite recursion!
}
try {
badFactorial(10);
} catch (StackOverflowError e) {
System.err.println("Stack overflow — check for infinite recursion");
}
// Correct — with base case:
public static long factorial(int n) {
if (n <= 1) return 1; // base case prevents infinite recursion
return n * factorial(n - 1);
}
// ── AssertionError — assertion failure (with -ea flag): ───────────────
public int divide(int a, int b) {
assert b != 0 : "Divisor must not be zero (called with b=" + b + ")";
return a / b;
}
// Run with -ea to enable assertions in development.
// AssertionError thrown if b == 0.
// ── NoClassDefFoundError — class found at compile time but not runtime: ─
// Example: code compiled with json-lib.jar in classpath, but jar missing at runtime.
// At runtime, accessing com.example.JsonParser throws NoClassDefFoundError.
// ClassNotFoundException: thrown by Class.forName() when class simply not found.
try {
Class.forName("com.example.OptionalClass");
} catch (ClassNotFoundException e) {
System.out.println("Optional class not available: " + e.getMessage());
// ClassNotFoundException is CHECKED — must be handled.
}When and How to Handle Errors
The general rule is clear: do not catch Error subclasses in normal application code. The conditions they represent are fundamentally different from exceptions — they signal that the runtime environment itself has broken down, not that the application encountered a predictable failure. Catching an Error and attempting to continue normal operation is almost always wrong and can produce unpredictable, dangerous behaviour.
That said, there are narrow legitimate use cases for catching specific Errors. Frameworks and containers sometimes catch OutOfMemoryError or StackOverflowError to log a diagnostic message and perform a clean shutdown rather than an abrupt crash. Test frameworks may catch AssertionError to record test failures. Web servers and application containers may catch Throwable at the outermost request-handling layer to ensure an HTTP 500 response is returned rather than a silent connection drop. In all these cases, the catch block should do minimal work — log, report, and shut down — not attempt to resume normal processing.
Catching Throwable (which catches both Exception and Error) is occasionally appropriate in framework code where you want to guarantee that any failure is logged before the JVM terminates. A top-level thread's uncaught exception handler (Thread.setDefaultUncaughtExceptionHandler) is the preferred mechanism for this because it receives Throwable without requiring a try-catch around all application code.
The try-with-resources statement and finally blocks are appropriate for cleanup (closing files, releasing locks, closing connections) even in the presence of Errors — these blocks run regardless of what Throwable was thrown. But the cleanup code should be simple and defensive, because it may itself be running in a critically degraded state.
Java
// ── Do NOT do this in normal application code: ───────────────────────
try {
processLargeDataset(data);
} catch (OutOfMemoryError e) {
System.out.println("Low memory — retrying with smaller batch");
processSmallBatch(data); // DANGEROUS — memory is critically low,
// this allocation may also fail,
// and data may be in corrupted state
}
// ── Legitimate narrow use — framework-level catch for graceful shutdown: ─
public class RequestHandler implements Runnable {
@Override
public void run() {
try {
handleRequest(request);
} catch (Exception e) {
// Normal exception handling:
logger.error("Request handling failed", e);
sendErrorResponse(500, "Internal server error");
} catch (Error e) {
// Narrow Error catch — minimal actions only:
try {
logger.error("JVM error during request handling", e);
sendErrorResponse(500, "Server error");
} catch (Throwable t) {
// Even this may fail — give up
}
// Do NOT try to continue — re-throw or exit:
throw e;
}
}
}
// ── Top-level uncaught exception handler — preferred approach: ────────
Thread.setDefaultUncaughtExceptionHandler((thread, throwable) -> {
System.err.println("Uncaught exception in thread " + thread.getName());
throwable.printStackTrace(System.err);
// Optional: alert monitoring, write crash report
System.exit(1); // clean exit
});
// ── finally still executes — use for cleanup: ─────────────────────────
Connection conn = null;
try {
conn = dataSource.getConnection();
// ... use connection ...
} catch (SQLException e) {
logger.error("SQL error", e);
} finally {
if (conn != null) {
try { conn.close(); } catch (SQLException ignored) { }
// Connection closed even if OutOfMemoryError was thrown
}
}
// ── Comparison summary: ───────────────────────────────────────────────
//
// Exception Error
// ────────────── ────────────────── ──────────────────────────
// Extends Throwable Throwable
// Represents Application failure JVM/environment failure
// Should catch? Yes (usually) No (almost never)
// Compiler check Checked subclasses Neither checked nor unchecked
// Recovery Often possible Rarely possible
// Common types IOException, NPE OOM, StackOverflow, NoClassDef
// Application Handle, log, retry Log and shut downRelated Topics in Exception Handling
Exception Basics
An exception is an event that disrupts the normal flow of a program during execution. Java's exception system is a structured mechanism for signalling, propagating, and handling error conditions — a significant improvement over the older approach of returning special error codes that callers could silently ignore. Understanding exceptions means understanding the class hierarchy that categorises them, the distinction between checked and unchecked exceptions, what the JVM does when an exception is thrown, how the call stack is unwound, and what information an exception object carries. This entry covers the full exception hierarchy, the checked vs unchecked distinction and the reasoning behind it, exception propagation through the call stack, and the information model of exception objects.
try-catch
The try-catch statement is Java's primary mechanism for handling exceptions. Code that might throw an exception is placed in the try block; one or more catch blocks follow, each specifying the exception type to handle and providing the handling logic. When an exception is thrown inside the try block, execution of the try block immediately stops and the JVM searches the catch blocks in order for the first one whose type matches the thrown exception. Understanding the control flow precisely — what runs, what stops, and in what order — is essential for writing correct exception handling code. This entry covers try-catch control flow, exception type matching, catching by supertype, variable scope, and the key principles for writing good catch blocks.
Multiple catch
A single try block can be followed by multiple catch blocks, each handling a different exception type. Multiple catch blocks allow code to respond differently to different kinds of failures — recovering from an expected missing file, but reporting and rethrowing an unexpected database error. Java 7 introduced multi-catch syntax, allowing a single catch block to handle multiple unrelated exception types, eliminating the duplicate code that arises when different exceptions require the same handling. This entry covers the ordering rules, multi-catch syntax, the type of the caught variable in multi-catch, and the design patterns for structuring multiple exception handlers.
finally
The finally block contains code that must execute regardless of whether the try block completed normally, threw an exception that was caught, or threw an exception that was not caught. It is the mechanism for guaranteed cleanup — closing files, releasing locks, returning connections to a pool, rolling back transactions. The finally block runs in all scenarios except two: if the JVM exits (System.exit()) or if the thread is killed by an Error like StackOverflowError. Understanding when exactly finally runs, the interaction between finally and return statements, exception suppression when finally itself throws, and the modern alternative of try-with-resources is essential for writing correct resource management code in Java.