☕ Java
Unchecked Exception
An unchecked exception is an exception that the Java compiler does not require you to handle or declare. Unchecked exceptions extend RuntimeException (or Error, which is a separate category). They represent programming errors — incorrect method arguments, violated preconditions, array index out of bounds, null pointer dereferences — conditions that should not occur in correctly written code. Because these conditions indicate code defects rather than anticipated operational failures, callers are not forced to write defensive handling code for them.
What Makes an Exception Unchecked
Unchecked exceptions extend RuntimeException, which itself extends Exception. The Java compiler treats RuntimeException and its subclasses specially — it does not require calling code to handle or declare them. A method can throw an unchecked exception without including it in a throws clause, and a caller is not required to catch it. The exception propagates up the call stack automatically until something catches it or the thread terminates.
This design reflects a philosophical distinction about the nature of failures. Checked exceptions model anticipated, recoverable failures that occur in the normal course of operating a program — a file might not be found, a network connection might be refused, user input might be malformed. These are conditions that a competent caller can and should handle. Unchecked exceptions model defects in the program itself — a null reference was dereferenced, an array index was negative, a method was called with an invalid argument. These conditions indicate that the program has a bug, and the correct response is to fix the bug, not to catch the exception.
The phrase "a caller cannot be expected to handle them" captures this distinction. If your method throws NullPointerException, it means your code (or the caller's code) has a bug. There is no meaningful way to "handle" a NullPointerException in the catch block — the correct action is to find and fix the null. In contrast, if your method throws FileNotFoundException, the caller can meaningfully respond by trying a different file path, using a default value, or asking the user to provide a valid path.
All RuntimeException subclasses in the standard library model this kind of programming error or violated precondition: NullPointerException for dereferencing null, ArrayIndexOutOfBoundsException for invalid indices, ClassCastException for invalid casts, IllegalArgumentException for invalid method arguments, IllegalStateException for calling a method when the object is in the wrong state, ArithmeticException for division by zero, and UnsupportedOperationException for operations that are not supported.
Java
// ── Unchecked exceptions do not require try-catch or throws: ─────────
public void processOrder(Order order) {
// No try-catch needed — NullPointerException is unchecked
String customerId = order.getCustomer().getId();
// No try-catch needed — IllegalArgumentException is unchecked
if (order.getQuantity() <= 0) {
throw new IllegalArgumentException(
"Quantity must be positive: " + order.getQuantity());
}
}
// Caller does not need to handle NullPointerException or IAE:
void run() {
processOrder(myOrder); // no try-catch required — compiles fine
}
// ── Contrast with checked: ────────────────────────────────────────────
// This does NOT compile without try-catch or throws:
public void readFile() {
new FileReader("data.txt"); // FileNotFoundException is CHECKED
}
// This DOES compile without handling:
public void validate(String input) {
if (input == null) throw new NullPointerException("input"); // unchecked
if (input.isBlank()) throw new IllegalArgumentException(); // unchecked
}
// ── Unchecked exception hierarchy: ───────────────────────────────────
RuntimeException rte = new RuntimeException("base");
NullPointerException npe = new NullPointerException("null ref");
IllegalArgumentException iae = new IllegalArgumentException("bad arg");
System.out.println(rte instanceof RuntimeException); // true
System.out.println(npe instanceof RuntimeException); // true
System.out.println(iae instanceof RuntimeException); // true
// All RuntimeExceptions are also Exceptions (and Throwables):
System.out.println(rte instanceof Exception); // true
System.out.println(rte instanceof Throwable); // trueCommon Unchecked Exceptions and Their Causes
The standard library's unchecked exceptions represent specific kinds of programming errors, each with a well-defined cause. Recognising these exceptions and understanding their causes is a fundamental debugging skill. Each has a specific meaning and a specific class of bug that produces it.
NullPointerException (NPE) is the most common Java exception. It occurs when code attempts to dereference a null reference — calling a method, accessing a field, or passing a null where a non-null is required. Java 14 introduced "helpful NullPointerExceptions" that include the name of the variable that was null and which operation on it failed, making NPEs much easier to diagnose. Modern code style prevents NPEs through defensive null checks at object creation, Optional for nullable values, and Objects.requireNonNull() in constructors.
ArrayIndexOutOfBoundsException occurs when code accesses an array element at an index that is less than zero or greater than or equal to the array's length. This is almost always an off-by-one error in loop bounds or index arithmetic. The exception message includes the invalid index and the array length, making it easy to diagnose.
ClassCastException occurs when code attempts to cast an object to a type it is not an instance of. The message includes both the actual type of the object and the type it was being cast to. The instanceof operator and pattern matching in modern Java provide safe alternatives to blind casting.
StackOverflowError is technically an Error (not RuntimeException) but behaves like an unchecked exception in practice. It occurs when the call stack is full — almost always because of unbounded recursion where a method calls itself without reaching a base case. The fix is to either add or correct the base case, or convert the recursion to iteration.
Java
// ── NullPointerException and how to prevent it: ──────────────────────
String name = null;
// name.length() → NullPointerException: Cannot invoke "String.length()"
// because "name" is null (Java 14+ helpful NPE)
// Prevention 1: null check:
if (name != null) {
System.out.println(name.length());
}
// Prevention 2: Optional:
Optional<String> optName = Optional.ofNullable(name);
int len = optName.map(String::length).orElse(0);
// Prevention 3: Objects.requireNonNull in constructor:
public class User {
private final String username;
public User(String username) {
this.username = Objects.requireNonNull(username, "username required");
}
}
// ── ArrayIndexOutOfBoundsException: ──────────────────────────────────
int[] arr = {10, 20, 30};
// arr[3] → ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3
// arr[-1] → ArrayIndexOutOfBoundsException: Index -1 out of bounds for length 3
// Always validate indices:
int index = 3;
if (index >= 0 && index < arr.length) {
System.out.println(arr[index]);
} else {
System.out.println("Index out of range");
}
// ── ClassCastException: ───────────────────────────────────────────────
Object obj = "Hello";
// Integer n = (Integer) obj; // ClassCastException: String cannot be cast to Integer
// Safe with instanceof:
if (obj instanceof Integer n) {
System.out.println("Integer: " + n);
} else {
System.out.println("Not an Integer — actual type: " +
obj.getClass().getSimpleName());
}
// ── IllegalArgumentException — document preconditions: ────────────────
public static int factorial(int n) {
if (n < 0) throw new IllegalArgumentException(
"n must be non-negative, got: " + n);
if (n == 0) return 1;
return n * factorial(n - 1);
}
// ── IllegalStateException — object in wrong state: ───────────────────
public class Connection {
private boolean open = false;
public void open() { open = true; }
public String query(String sql) {
if (!open) throw new IllegalStateException(
"Cannot query a closed connection");
return "result";
}
}
// ── ArithmeticException — division by zero: ───────────────────────────
// int result = 10 / 0; // ArithmeticException: / by zero
// (Note: floating point division by 0.0 does NOT throw — returns Infinity or NaN)
int divisor = 0;
if (divisor == 0) throw new ArithmeticException("Division by zero");When to Use Unchecked vs Checked
The decision between checked and unchecked exceptions is one of the most important API design decisions in Java and one of the most debated. The two camps can be summarised as: use checked for failures the caller can meaningfully recover from; use unchecked for programming errors and failures where recovery is not generally expected.
The practical guideline that produces the clearest APIs is: if a reasonable caller could write specific, meaningful code in the catch block to recover from this exception — not just log and rethrow — make it checked. If the only meaningful response is to log the exception, display an error message, and fail, make it unchecked.
Spring, Hibernate, and most modern Java frameworks use unchecked exceptions throughout their APIs. The rationale is that in application-layer code, most exceptions that occur during operations (database failures, network timeouts, invalid data) cannot be meaningfully handled at the point of occurrence — they need to propagate to a global error handler. Forcing every caller to declare and catch these exceptions adds verbosity without adding value. A global exception handler catches RuntimeException at the top level and translates it to an appropriate HTTP response or error message.
The "translate at the boundary" pattern is a useful middle ground. Internal layers use unchecked exceptions for flexibility and clean code. At the boundary between layers — where a service layer method's result is returned to a controller, or where an API's response is generated — a global handler catches unchecked exceptions and translates them to the appropriate response format. This gives the benefits of unchecked exceptions (clean code, no forced handling) while still ensuring failures are handled meaningfully.
Java
// ── Decision guide: ──────────────────────────────────────────────────
//
// Use CHECKED when:
// The caller has a recovery strategy specific to this failure
// The failure is an expected operational condition (not a bug)
// Forcing the caller to acknowledge the failure adds real value
// Examples: FileNotFoundException, SQLException, ParseException
//
// Use UNCHECKED when:
// The failure indicates a programming error (wrong arg, null, bad state)
// The failure should never occur in correct code
// Most callers would just rethrow or log — no meaningful recovery
// You are building a framework where checked exceptions add verbosity
// Examples: NullPointerException, IllegalArgumentException, all custom
// domain exceptions in Spring-style applications
// ── Spring-style: unchecked domain exceptions + global handler: ───────
@RestController
public class OrderController {
@GetMapping("/orders/{id}")
public OrderDto getOrder(@PathVariable String id) {
// No try-catch — OrderNotFoundException is unchecked.
// If the order does not exist, the exception propagates
// to the global handler below.
return orderService.findById(id);
}
}
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(OrderNotFoundException.class)
public ResponseEntity<ErrorDto> handleNotFound(OrderNotFoundException e) {
return ResponseEntity.status(404)
.body(new ErrorDto("NOT_FOUND", e.getMessage()));
}
@ExceptionHandler(ValidationException.class)
public ResponseEntity<ErrorDto> handleValidation(ValidationException e) {
return ResponseEntity.status(400)
.body(new ErrorDto("VALIDATION_FAILED", e.getMessage()));
}
@ExceptionHandler(RuntimeException.class)
public ResponseEntity<ErrorDto> handleGeneral(RuntimeException e) {
logger.error("Unhandled exception", e);
return ResponseEntity.status(500)
.body(new ErrorDto("INTERNAL_ERROR", "An unexpected error occurred"));
}
}Related 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.