What is an Exception?
An exception is an object that represents an abnormal condition
during program execution. When something goes wrong β a file doesn't exist, a
network connection drops, someone passes null where a value was
expected β the JVM (or your own code) throws an exception object.
This interrupts the normal execution flow immediately.
Without exception handling, that interruption crashes the program and the user sees a raw stack trace. With exception handling, you can catch the exception, react to it in a controlled way, and decide whether to recover, report, or propagate it up the call stack.
// Without exception handling: program crashes
int[] numbers = {1, 2, 3};
System.out.println(numbers[10]); // β THROWS ArrayIndexOutOfBoundsException
System.out.println("this never runs");
// Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException:
// Index 10 out of bounds for length 3
// With exception handling: you control what happens
try {
System.out.println(numbers[10]); // β throws, jumps to catch immediately
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("Index does not exist: " + e.getMessage());
}
System.out.println("this DOES run"); // execution continues normally
Throwing vs catching
Throwing means signalling that something went wrong. You use the
throw keyword with an exception object. Execution at the throw site
stops immediately β no more code in that method runs after a throw.
Catching means intercepting a thrown exception before it crashes
the program. You use try { } catch { }. The catch block only runs
if an exception of the matching type was thrown inside the try block.
// THROWING β you signal the problem
public void validateAge(int age) {
if (age < 0)
throw new IllegalArgumentException("Age cannot be negative: " + age);
// nothing below this line runs if age < 0
this.age = age;
}
// CATCHING β you handle the problem
try {
validateAge(-5);
} catch (IllegalArgumentException e) {
System.out.println("Bad input: " + e.getMessage());
// decide: fix it, log it, re-throw it, show a message to the user...
}
Runtime exceptions vs compile-time exceptions
Java distinguishes two kinds:
- Checked exceptions β the compiler knows this operation can
fail and forces you to deal with it. You must either catch it or
declare it with
throwsin the method signature. Examples: reading a file (IOException), querying a database (SQLException). The idea: these are recoverable external failures that the caller should be aware of. - Unchecked exceptions (runtime exceptions) β the compiler
does not force you to handle them. They represent programming bugs or
violated preconditions:
NullPointerException,IllegalArgumentException,IndexOutOfBoundsException. The idea: if your code is correct, these shouldn't happen β so forcing every caller to wrap every method call in try-catch would be noise.
// CHECKED β compiler rejects this without try-catch or throws declaration
public void readFile(String path) {
new FileReader(path); // compile error: FileNotFoundException not handled
}
// UNCHECKED β compiles fine, but may blow up at runtime
public void printLength(String s) {
System.out.println(s.length()); // NullPointerException if s is null
}
An exception is a regular Java object β it has fields and methods. The most
useful ones: getMessage() returns the human-readable description,
getCause() returns the original exception that triggered this one
(if any), and printStackTrace() prints the full call stack at the
moment of the throw β essential for debugging.
The Exception Hierarchy
In Java, everything throwable is a subclass of Throwable. The hierarchy
determines what you must handle, what you can ignore, and what you should never catch.
/*
* Throwable
* βββ Error β JVM problems, do NOT catch
* β βββ OutOfMemoryError
* β βββ StackOverflowError
* β βββ AssertionError
* β
* βββ Exception
* βββ IOException β CHECKED: compiler forces you to handle
* β βββ FileNotFoundException
* β βββ SocketException
* βββ SQLException β CHECKED
* βββ ClassNotFoundException β CHECKED
* β
* βββ RuntimeException β UNCHECKED: optional to handle
* βββ NullPointerException
* βββ IllegalArgumentException
* βββ IllegalStateException
* βββ UnsupportedOperationException
* βββ ArithmeticException
* βββ ClassCastException
* βββ IndexOutOfBoundsException
* β βββ ArrayIndexOutOfBoundsException
* βββ NumberFormatException
*/
| Category | Must handle? | Typical cause | Examples |
|---|---|---|---|
| Error | β Never catch | JVM / system failure | OutOfMemoryError, StackOverflowError |
| Checked Exception | β Yes (catch or declare) | External resources | IOException, SQLException |
| Unchecked (Runtime) | Optional | Programming bugs | NullPointerException, IllegalArgumentException |
try / catch / finally
try {
// code that may throw
int result = 100 / divisor;
processResult(result);
} catch (ArithmeticException e) {
// specific exception first
log.error("Division failed", e);
} catch (IllegalArgumentException | IllegalStateException e) {
// multi-catch: same handler for two unrelated exceptions (Java 7+)
log.warn("Validation error", e);
} catch (Exception e) {
// general catch last β order matters: most specific first
log.error("Unexpected error", e);
throw e; // re-throw if you can't handle it here
} finally {
// ALWAYS runs β with or without exception, even with return in try/catch
// Only exception: System.exit() or JVM crash
cleanup();
}
// β Unreachable catch β Exception catches everything above it
catch (Exception e) { }
catch (IOException e) { } // compile error: already caught by Exception
// β
Most specific first, most general last
catch (FileNotFoundException e) { } // subtype of IOException
catch (IOException e) { } // catches remaining IO exceptions
catch (Exception e) { } // last resort
try-with-resources β automatic close (Java 7+)
Any object implementing AutoCloseable can go in the parentheses.
It is closed automatically at the end of the block, even if an exception is thrown.
// β Old way: fragile, easy to forget close in the exception path
Connection conn = null;
try {
conn = dataSource.getConnection();
// use conn...
} finally {
if (conn != null) try { conn.close(); } catch (SQLException ignored) {}
}
// β
try-with-resources: clean, guaranteed close, multiple resources OK
try (Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement("SELECT 1");
ResultSet rs = ps.executeQuery()) {
while (rs.next()) { ... }
} catch (SQLException e) {
// conn, ps, rs all closed before we get here β in reverse order
throw new RuntimeException("DB query failed", e);
}
Checked vs Unchecked Exceptions
Checked β compiler enforces handling
// Checked exceptions extend Exception (but NOT RuntimeException)
// You MUST either catch them or declare them with throws
// Option 1: catch and handle
public String readFile(String path) {
try (BufferedReader r = new BufferedReader(new FileReader(path))) {
return r.lines().collect(Collectors.joining("\n"));
} catch (IOException e) {
log.error("Cannot read {}", path, e);
return "";
}
}
// Option 2: declare and let the caller decide
public String readFile(String path) throws IOException {
try (BufferedReader r = new BufferedReader(new FileReader(path))) {
return r.lines().collect(Collectors.joining("\n"));
}
}
Unchecked β for programming errors and validation failures
// Unchecked = extend RuntimeException
// Use for: invalid arguments, broken preconditions, bugs
// The compiler does NOT force callers to handle them
public void setAge(int age) {
if (age < 0 || age > 150)
throw new IllegalArgumentException("Invalid age: " + age);
this.age = age;
}
// Most useful unchecked exceptions from the JDK:
throw new IllegalArgumentException("bad input"); // invalid parameter value
throw new IllegalStateException("not connected"); // method called at wrong time
throw new NullPointerException("name is null"); // explicit null check (rare)
throw new UnsupportedOperationException("not implemented"); // stub
throw new IndexOutOfBoundsException("index: " + i); // invalid index
// Objects.requireNonNull β standard null guard (throws NullPointerException)
this.name = Objects.requireNonNull(name, "name must not be null");
Custom Exceptions
Create domain-specific exceptions when standard JDK types don't convey enough
meaning. The rule: extend RuntimeException for programming errors /
unrecoverable situations; extend Exception for recoverable situations
where callers should be forced to handle.
// Custom unchecked exception β callers don't have to catch it
public class InsufficientFundsException extends RuntimeException {
private final double available;
private final double requested;
public InsufficientFundsException(double available, double requested) {
super(String.format("Insufficient funds: available=%.2f, requested=%.2f",
available, requested));
this.available = available;
this.requested = requested;
}
// Always provide a cause-accepting constructor for exception chaining
public InsufficientFundsException(double available, double requested,
Throwable cause) {
super(String.format("Insufficient funds: available=%.2f, requested=%.2f",
available, requested), cause);
this.available = available;
this.requested = requested;
}
public double getAvailable() { return available; }
public double getRequested() { return requested; }
}
// Usage β caller gets rich context, not just a message string
public void withdraw(double amount) {
if (amount > balance)
throw new InsufficientFundsException(balance, amount);
balance -= amount;
}
try {
account.withdraw(500);
} catch (InsufficientFundsException e) {
System.out.printf("Need %.2f more%n", e.getRequested() - e.getAvailable());
}
Exception Chaining
When you catch an exception and throw a different one, always pass the original as the cause. Without it, the root cause disappears from the stack trace β a debugging nightmare.
// β BAD: original cause lost β the stack trace shows only OrderException,
// you can never find out WHAT SQLException triggered it
try {
db.save(order);
} catch (SQLException e) {
throw new OrderException("Failed to save order"); // cause dropped!
}
// β
GOOD: wrap and preserve the cause
try {
db.save(order);
} catch (SQLException e) {
throw new OrderException("Failed to save order", e); // e is the cause
}
// When you inspect the exception, the full chain is visible:
// OrderException: Failed to save order
// Caused by: java.sql.SQLException: Connection refused
// Caused by: java.net.ConnectException: Connection refused
// Retrieve the cause programmatically:
e.getCause(); // direct cause
ExceptionUtils.getRootCause(e); // root cause (Apache Commons)
Common Exceptions Reference
| Exception | Type | When it occurs | Prevention |
|---|---|---|---|
NullPointerException |
Unchecked | Method called on null reference |
Objects.requireNonNull, Optional |
IllegalArgumentException |
Unchecked | Parameter fails precondition | Validate in method/constructor |
IllegalStateException |
Unchecked | Method called in wrong state | Guard with state check |
IndexOutOfBoundsException |
Unchecked | Invalid array/list index | Check size() / length first |
ClassCastException |
Unchecked | Invalid cast | Use instanceof before casting |
NumberFormatException |
Unchecked | parseInt("abc") |
Validate input before parsing |
IOException |
Checked | File / network / stream error | try-with-resources |
SQLException |
Checked | Database operation failed | Wrap in domain exception with cause |
StackOverflowError |
Error | Infinite recursion | Ensure base case in recursion |
OutOfMemoryError |
Error | Heap exhausted | Fix memory leaks, tune heap |
Common Pitfalls
// β Empty catch: the error disappears, the bug hides forever
try {
processOrder(order);
} catch (Exception e) {
// nothing
}
// β
At minimum: log. If you truly can't handle it, re-throw.
try {
processOrder(order);
} catch (OrderException e) {
log.error("Order processing failed for orderId={}", order.getId(), e);
throw e; // or wrap in a higher-level exception
}
// β BAD: exceptions are expensive β stack trace captured on construction
try {
int value = map.get("key").intValue(); // may NPE if key absent
} catch (NullPointerException e) {
value = 0; // using exception to handle "not found" case
}
// β
GOOD: check first, exception-free path for the common case
int value = map.getOrDefault("key", 0);
// β NEVER catch Error or raw Throwable in normal application code
catch (Throwable t) { ... } // catches OutOfMemoryError, StackOverflowErrorβ¦
catch (Error e) { ... } // the JVM is broken β you can't recover
// β
Only catch what you can actually handle
// Exception: frameworks (Spring, JUnit) catch Throwable intentionally β that's fine
// β Root cause gone β impossible to diagnose in production
catch (SQLException e) {
throw new ServiceException("DB error"); // no cause!
}
// β
Always chain the cause
catch (SQLException e) {
throw new ServiceException("DB error", e); // full chain preserved
}
Senior Topics: Exceptions in Modern Java
Exception handling in Streams and lambdas
Lambdas cannot throw checked exceptions β functional interface methods don't declare them. This is a real pain with streams over I/O operations.
// β Doesn't compile: Files::readString throws IOException (checked)
List<String> contents = paths.stream()
.map(Files::readString) // compile error: unhandled IOException
.toList();
// Option 1: wrap checked in unchecked inside the lambda
List<String> contents = paths.stream()
.map(path -> {
try { return Files.readString(path); }
catch (IOException e) { throw new UncheckedIOException(e); }
})
.toList();
// Option 2: extract to a helper that wraps (reusable)
private static <T, R> Function<T, R> wrap(CheckedFunction<T, R> fn) {
return t -> {
try { return fn.apply(t); }
catch (Exception e) { throw new RuntimeException(e); }
};
}
// Usage: paths.stream().map(wrap(Files::readString)).toList();
// Option 3 (Java 21+): use vavr or Result types for functional error handling
Suppressed exceptions in try-with-resources
// If both the try body AND close() throw, close()'s exception is "suppressed"
// The primary exception propagates; the suppressed one is attached
try (MyResource r = new MyResource()) {
r.riskyOperation(); // throws ExceptionA
// r.close() called automatically β throws ExceptionB
// ExceptionA propagates; ExceptionB is attached as suppressed
}
// Inspect suppressed exceptions:
try { ... }
catch (Exception primary) {
for (Throwable suppressed : primary.getSuppressed()) {
log.error("Suppressed: {}", suppressed.getMessage());
}
throw primary;
}
Global exception handling in Spring
// Instead of try-catch in every controller, centralise with @ControllerAdvice
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(InsufficientFundsException.class)
public ResponseEntity<ErrorResponse> handle(InsufficientFundsException e) {
return ResponseEntity
.status(HttpStatus.UNPROCESSABLE_ENTITY)
.body(new ErrorResponse("INSUFFICIENT_FUNDS", e.getMessage()));
}
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<ErrorResponse> handle(IllegalArgumentException e) {
return ResponseEntity.badRequest()
.body(new ErrorResponse("INVALID_INPUT", e.getMessage()));
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleUnexpected(Exception e) {
log.error("Unhandled exception", e);
return ResponseEntity.internalServerError()
.body(new ErrorResponse("INTERNAL_ERROR", "Please try later"));
}
}
Interview Questions
Q: What is the difference between checked and unchecked exceptions?
Checked exceptions extend Exception (but not RuntimeException).
The compiler forces callers to either catch them or declare them with throws.
Unchecked exceptions extend RuntimeException β handling is optional.
Use checked for recoverable external failures (file not found, network timeout);
use unchecked for programming errors and failed preconditions.
Q: What is the purpose of the finally block?
Code in finally always runs: after normal completion, after a caught
exception, and even after an uncaught exception unwinds the stack. The only
exceptions: System.exit() and JVM crash. Its primary use is resource
cleanup β though try-with-resources is the modern replacement.
Q: What does try-with-resources do?
Any AutoCloseable declared in the parentheses is automatically closed
at block exit, even on exception. Multiple resources are closed in reverse order
of declaration. If both the body and close() throw, the close exception
is suppressed and attached to the primary exception via
getSuppressed().
Q: Should you use checked or unchecked exceptions for domain errors?
This is one of Java's great debates. The original intent: checked for recoverable
situations the caller must handle. In practice, most modern frameworks and libraries
(Spring, Hibernate, JUnit) prefer unchecked because checked exceptions pollute
call stacks (every intermediate layer must declare or wrap them), break lambdas
(functional interfaces don't declare checked exceptions), and the contract
enforcement argument is weak β callers often swallow checked exceptions with empty
catches. Effective Java Item 71: "use unchecked exceptions for programming errors;
consider checked for recoverable conditions only when the caller can reasonably
be expected to handle them."
Q: Why is exception swallowing dangerous and how do you detect it?
An empty catch block (or one that only logs without re-throwing or
taking corrective action) silently absorbs the error. The system enters an unknown
state with no trace of what happened. Tools: SpotBugs / PMD flag empty catches;
SonarQube rules DE_MIGHT_IGNORE and REC_CATCH_EXCEPTION; code review. The rule:
if you catch an exception and can't fully handle it, re-throw it (as-is, or wrapped).
Q: What are suppressed exceptions and when do they occur?
When try-with-resources closes a resource, if both the try body and
close() throw, the close exception is suppressed β attached
to the primary exception rather than replacing it. Pre-Java 7, the close exception
would overwrite the primary one, silently losing the real error. Access them via
Throwable.getSuppressed(). This is why try-with-resources is safer
than manual finally blocks for resource closing.
Q: How do you handle checked exceptions in lambda expressions?
You can't β functional interface methods don't declare checked exceptions, so the
compiler rejects them. Options: (1) wrap in unchecked inside the lambda β
verbose but explicit; (2) create a utility wrap() method that converts
any CheckedFunction to Function; (3) use
UncheckedIOException for IOException specifically β
it's a standard JDK wrapper. Libraries like Vavr and Lombok's
@SneakyThrows provide more elegant solutions.