Exception Handling in Java
1. What is an Exception?
An exception is an abnormal event that disrupts the normal flow of a program at runtime. Java's exception handling mechanism lets you detect, handle, and recover from such events without crashing the program.
Java distinguishes three categories of throwable problems:
| Category | Superclass | Checked? | Examples | When Occurs |
|---|---|---|---|---|
| Checked Exception | Exception |
Yes — compiler enforces handling |
IOException,
SQLException,
ParseException
|
Predictable failures: file not found, bad URL |
| Unchecked Exception | RuntimeException |
No — optional to handle |
NullPointerException,
ArrayIndexOutOfBoundsException, ClassCastException
|
Programming bugs / logic errors |
| Error | Error |
No — not meant to be caught |
OutOfMemoryError,
StackOverflowError
|
Serious JVM problems — typically unrecoverable |
Checked vs Unchecked: Checked exceptions must
be either caught (try-catch) or
declared in the method signature (throws). Unchecked exceptions (RuntimeException subclasses) have no
such requirement — handling is optional.
2. Exception Hierarchy
All exceptions and errors in Java descend from
java.lang.Throwable. Understanding
this hierarchy helps you choose which exceptions to catch and how
broadly.
java.lang.Object
+-- java.lang.Throwable
+-- java.lang.Error (Errors — don't catch these)
¦ +-- OutOfMemoryError
¦ +-- StackOverflowError
¦ +-- VirtualMachineError
+-- java.lang.Exception (Checked exceptions)
+-- IOException
¦ +-- FileNotFoundException
¦ +-- EOFException
+-- SQLException
+-- ParseException
+-- ClassNotFoundException
+-- RuntimeException (Unchecked exceptions)
+-- NullPointerException
+-- ArrayIndexOutOfBoundsException
+-- ClassCastException
+-- NumberFormatException
+-- IllegalArgumentException
+-- IllegalStateException
+-- ArithmeticException
+-- UnsupportedOperationException
| Exception | Type | Common Cause |
|---|---|---|
NullPointerException |
Unchecked | Calling a method on a null reference |
ArrayIndexOutOfBoundsException
|
Unchecked | Accessing array index outside valid range |
NumberFormatException |
Unchecked |
Integer.parseInt("abc") — non-numeric string
|
ClassCastException |
Unchecked |
Invalid type casting: (String) new Integer(1)
|
ArithmeticException |
Unchecked | Division by zero: 10 / 0 |
FileNotFoundException |
Checked | Opening a file that doesn't exist |
IOException |
Checked | General I/O failure (network, disk) |
SQLException |
Checked | Database query or connection failure |
StackOverflowError |
Error | Infinite or deeply nested recursion |
3. try-catch Block
The try block contains code that might
throw an exception. The catch block
handles it. Java checks catch blocks
top to bottom — put more specific exceptions
before more general ones.
public class TryCatchDemo {
// Basic try-catch
public static void basicDemo() {
try {
int result = 10 / 0; // throws ArithmeticException
System.out.println(result); // never reached
} catch (ArithmeticException e) {
System.out.println("Caught: " + e.getMessage()); // / by zero
}
System.out.println("Program continues...");
}
// Catching specific exceptions — order matters (specific before general)
public static void multiCatchDemo(String input, int[] arr, int index) {
try {
int parsed = Integer.parseInt(input); // may throw NumberFormatException
int value = arr[index]; // may throw ArrayIndexOutOfBoundsException
int result = parsed / value; // may throw ArithmeticException
System.out.println("Result: " + result);
} catch (NumberFormatException e) {
System.out.println("Bad number format: " + e.getMessage());
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("Array index error: " + e.getMessage());
} catch (ArithmeticException e) {
System.out.println("Math error: " + e.getMessage());
} catch (Exception e) {
// General catch-all — always last
System.out.println("Unexpected error: " + e.getMessage());
}
}
// Multi-catch (Java 7+) — catch multiple types in one block
public static void multiCatchShorthand(String input) {
try {
int val = Integer.parseInt(input);
System.out.println("Parsed: " + val);
} catch (NumberFormatException | NullPointerException e) {
System.out.println("Input error: " + e.getClass().getSimpleName());
}
}
public static void main(String[] args) {
basicDemo();
System.out.println("---");
multiCatchDemo("abc", new int[]{1,2,3}, 1); // NumberFormatException
multiCatchDemo("5", new int[]{1,2,3}, 5); // ArrayIndexOutOfBounds
multiCatchDemo("5", new int[]{0,0,0}, 0); // ArithmeticException (5/0)
multiCatchDemo("5", new int[]{2,4,6}, 1); // Success: 5/4 = 1
System.out.println("---");
multiCatchShorthand(null); // NullPointerException
multiCatchShorthand("xyz"); // NumberFormatException
}
}
Program continues...
---
Bad number format: For input string: "abc"
Array index error: Index 5 out of bounds for length 3
Math error: / by zero
Result: 1
---
Input error: NullPointerException
Input error: NumberFormatException
Never catch Exception or
Throwable silently
with an empty catch block — it hides bugs and makes debugging
very difficult. Always log or handle the exception meaningfully.
4. The finally Block
The finally block
always executes after the try and any catch
blocks — whether or not an exception was thrown or caught. It is
the ideal place to release resources like file handles or database
connections.
import java.io.*;
public class FinallyDemo {
// finally always runs
public static void alwaysRuns(boolean throwEx) {
try {
System.out.println("try block executing");
if (throwEx) {
throw new RuntimeException("Deliberate error");
}
System.out.println("No exception thrown");
} catch (RuntimeException e) {
System.out.println("catch: " + e.getMessage());
} finally {
System.out.println("finally: always executes");
}
}
// Classic resource cleanup pattern (pre Java 7)
public static void readFile(String path) {
BufferedReader reader = null;
try {
reader = new BufferedReader(new FileReader(path));
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
} catch (FileNotFoundException e) {
System.out.println("File not found: " + path);
} catch (IOException e) {
System.out.println("IO error: " + e.getMessage());
} finally {
// This runs even if exception occurs — prevents resource leak
if (reader != null) {
try {
reader.close();
System.out.println("Reader closed in finally.");
} catch (IOException e) {
System.out.println("Error closing reader: " + e.getMessage());
}
}
}
}
public static void main(String[] args) {
alwaysRuns(false);
System.out.println("---");
alwaysRuns(true);
System.out.println("---");
readFile("nonexistent.txt");
}
}
No exception thrown
finally: always executes
---
try block executing
catch: Deliberate error
finally: always executes
---
File not found: nonexistent.txt
Special case:
finally does NOT execute if
System.exit() is called inside the
try block, or if the JVM crashes. For resource cleanup, prefer
try-with-resources (section 12) over explicit
finally.
5. The throw Keyword
Use throw to
manually throw an exception from your code. You
can throw any instance of a class that extends
Throwable, but in practice you throw
subclasses of Exception or
RuntimeException.
public class ThrowDemo {
public static double divide(double a, double b) {
if (b == 0) {
throw new ArithmeticException("Cannot divide by zero");
}
return a / b;
}
public static int getElement(int[] arr, int index) {
if (arr == null) {
throw new NullPointerException("Array must not be null");
}
if (index < 0 || index >= arr.length) {
throw new ArrayIndexOutOfBoundsException(
"Index " + index + " out of bounds for length " + arr.length);
}
return arr[index];
}
public static void setAge(int age) {
if (age < 0 || age > 150) {
throw new IllegalArgumentException(
"Age must be between 0 and 150, got: " + age);
}
System.out.println("Age set to: " + age);
}
public static void main(String[] args) {
// Successful calls
System.out.println(divide(10, 2)); // 5.0
System.out.println(getElement(new int[]{10, 20, 30}, 1)); // 20
setAge(25);
// Each throw caught and handled
try { divide(5, 0); }
catch (ArithmeticException e) { System.out.println("Error: " + e.getMessage()); }
try { getElement(new int[]{1,2,3}, 10); }
catch (ArrayIndexOutOfBoundsException e) { System.out.println("Error: " + e.getMessage()); }
try { setAge(-5); }
catch (IllegalArgumentException e) { System.out.println("Error: " + e.getMessage()); }
}
}
20
Age set to: 25
Error: Cannot divide by zero
Error: Index 10 out of bounds for length 3
Error: Age must be between 0 and 150, got: -5
6. The throws Keyword
throws is used in a method signature
to declare that the method may throw one or more checked
exceptions. This passes the responsibility of handling the
exception to the caller.
import java.io.*;
import java.net.*;
public class ThrowsDemo {
// Declares it may throw IOException — caller must handle it
public static String readFirstLine(String filename) throws IOException {
BufferedReader reader = new BufferedReader(new FileReader(filename));
String line = reader.readLine();
reader.close();
return line;
}
// Declares multiple checked exceptions
public static void fetchData(String urlStr) throws IOException, MalformedURLException {
URL url = new URL(urlStr); // may throw MalformedURLException
url.openConnection(); // may throw IOException
System.out.println("Connected to: " + urlStr);
}
// Caller MUST handle or re-declare with throws
public static void caller() throws IOException {
String line = readFirstLine("data.txt"); // handled by re-declaring
System.out.println("Read: " + line);
}
// Or catch it here
public static void callerWithCatch() {
try {
String line = readFirstLine("data.txt");
System.out.println("Read: " + line);
} catch (IOException e) {
System.out.println("Could not read file: " + e.getMessage());
}
}
public static void main(String[] args) {
callerWithCatch(); // handles IOException internally
try {
fetchData("not-a-valid-url");
} catch (IOException e) {
System.out.println("Fetch failed: " + e.getClass().getSimpleName()
+ " - " + e.getMessage());
}
}
}
Fetch failed: MalformedURLException - no protocol: not-a-valid-url
throw vs throws:
throw is an action — it actually
throws an exception object.
throws is a declaration — it
announces that a method might throw that exception. They are
used in completely different places.
7. final vs finally vs finalize
These three keywords sound similar but serve completely different purposes — a very common interview topic.
| Keyword | What It Is | Used With | Purpose |
|---|---|---|---|
final |
Access modifier keyword | Variables, methods, classes | Variable: constant (cannot reassign). Method: cannot be overridden. Class: cannot be subclassed. |
finally |
Block in exception handling | try-catch statement | Code that always executes after try/catch — used for cleanup. |
finalize() |
Instance method | Object (deprecated since Java 9) | Called by GC before reclaiming an object — unreliable, avoid using. |
public class FinalKeywords {
// final variable — constant, must be assigned once
final int MAX = 100;
// final method — cannot be overridden in subclasses
public final void display() {
System.out.println("final method: MAX = " + MAX);
}
public static void finalDemo() {
// final local variable
final String NAME = "Java";
// NAME = "Python"; // COMPILE ERROR — cannot reassign final
// final reference — the reference is final, not the object
final StringBuilder sb = new StringBuilder("Hello");
sb.append(", World!"); // OK — object is mutable
// sb = new StringBuilder(); // COMPILE ERROR
System.out.println("final ref: " + sb);
}
public static void finallyDemo() {
try {
System.out.println("In try");
int x = 5 / 1;
} finally {
System.out.println("finally always runs");
}
}
// finalize() — deprecated, called by GC before collection
// DO NOT rely on this for resource cleanup
@Override
@SuppressWarnings("deprecation")
protected void finalize() throws Throwable {
System.out.println("finalize() called by GC");
super.finalize();
}
public static void main(String[] args) {
finalDemo();
finallyDemo();
new FinalKeywords().display();
}
}
// final class — cannot be extended
final class ImmutablePoint {
final int x, y;
ImmutablePoint(int x, int y) { this.x = x; this.y = y; }
}
// class ExtendedPoint extends ImmutablePoint { } // COMPILE ERROR
In try
finally always runs
final method: MAX = 100
8. Custom Exceptions
You can create your own exception classes to represent
domain-specific error conditions. Extend
Exception for a checked exception, or
RuntimeException for an unchecked one.
// Checked custom exception — caller must handle or declare
public class InsufficientFundsException extends Exception {
private double amount;
private double balance;
public InsufficientFundsException(double amount, double balance) {
super(String.format("Cannot withdraw $%.2f. Available balance: $%.2f", amount, balance));
this.amount = amount;
this.balance = balance;
}
public double getAmount() { return amount; }
public double getBalance() { return balance; }
}
// Unchecked custom exception — optional to handle
public class InvalidAgeException extends RuntimeException {
private int invalidAge;
public InvalidAgeException(int age) {
super("Invalid age: " + age + ". Must be between 0 and 150.");
this.invalidAge = age;
}
public int getInvalidAge() { return invalidAge; }
}
public class BankAccount {
private String owner;
private double balance;
public BankAccount(String owner, double balance) {
this.owner = owner;
this.balance = balance;
}
// Checked exception — declared with throws
public void withdraw(double amount) throws InsufficientFundsException {
if (amount > balance) {
throw new InsufficientFundsException(amount, balance);
}
balance -= amount;
System.out.println("Withdrew $" + amount + ". New balance: $" + balance);
}
}
public class Person {
private String name;
private int age;
public Person(String name, int age) {
if (age < 0 || age > 150) {
throw new InvalidAgeException(age); // unchecked
}
this.name = name;
this.age = age;
}
@Override
public String toString() { return name + ", age " + age; }
}
public class Main {
public static void main(String[] args) {
BankAccount acc = new BankAccount("Alice", 500.0);
try {
acc.withdraw(200.0); // OK
acc.withdraw(400.0); // throws InsufficientFundsException
} catch (InsufficientFundsException e) {
System.out.println("Error: " + e.getMessage());
System.out.println("Tried to withdraw: $" + e.getAmount());
System.out.println("Available: $" + e.getBalance());
}
System.out.println("---");
try {
Person p1 = new Person("Bob", 30); // OK
System.out.println(p1);
Person p2 = new Person("Eve", -5); // throws InvalidAgeException
} catch (InvalidAgeException e) {
System.out.println("Age error: " + e.getMessage());
System.out.println("Invalid value was: " + e.getInvalidAge());
}
}
}
Error: Cannot withdraw $400.00. Available balance: $300.00
Tried to withdraw: $400.0
Available: $300.0
---
Bob, age 30
Age error: Invalid age: -5. Must be between 0 and 150.
Invalid value was: -5
9. Chained Exceptions
Exception chaining allows you to wrap a lower-level exception inside a higher-level one, preserving the original cause. This is essential for debugging layered applications — you can see the full chain of what went wrong.
public class DatabaseException extends Exception {
public DatabaseException(String message, Throwable cause) {
super(message, cause); // cause parameter chains the original exception
}
}
public class ServiceException extends Exception {
public ServiceException(String message, Throwable cause) {
super(message, cause);
}
}
public class DataLayer {
public void queryDatabase() throws DatabaseException {
try {
// Simulate a low-level SQL error
throw new java.sql.SQLException("Table 'users' does not exist");
} catch (java.sql.SQLException e) {
// Wrap low-level exception in domain exception, preserving cause
throw new DatabaseException("Database query failed", e);
}
}
}
public class ServiceLayer {
private DataLayer dataLayer = new DataLayer();
public void getUser(int id) throws ServiceException {
try {
dataLayer.queryDatabase();
} catch (DatabaseException e) {
// Wrap again — service doesn't expose DB details
throw new ServiceException("Could not retrieve user with id=" + id, e);
}
}
}
public class Main {
public static void main(String[] args) {
ServiceLayer service = new ServiceLayer();
try {
service.getUser(42);
} catch (ServiceException e) {
System.out.println("Service Error: " + e.getMessage());
// getCause() retrieves the chained exception
Throwable cause1 = e.getCause();
System.out.println("Caused by: " + cause1.getMessage());
Throwable cause2 = cause1.getCause();
System.out.println("Root cause: " + cause2.getMessage());
System.out.println("Root type: " + cause2.getClass().getSimpleName());
}
System.out.println("---");
// Using initCause() — alternative to cause constructor param
RuntimeException re = new RuntimeException("Outer exception");
IllegalStateException inner = new IllegalStateException("Inner exception");
re.initCause(inner);
try {
throw re;
} catch (RuntimeException e) {
System.out.println(e.getMessage());
System.out.println("initCause: " + e.getCause().getMessage());
}
}
}
Caused by: Database query failed
Root cause: Table 'users' does not exist
Root type: SQLException
---
Outer exception
initCause: Inner exception
Best practice: Always chain exceptions when re-throwing. Never swallow the original exception. This preserves the full stack trace which is invaluable during debugging in production.
10. NullPointerException
NullPointerException (NPE) is the most
common runtime exception in Java. It is thrown when you attempt to
use a reference that points to null.
Common Causes
public class NpeDemo {
public static void main(String[] args) {
// Cause 1: Calling method on null reference
String s = null;
// System.out.println(s.length()); // NPE!
// Cause 2: Accessing field of null object
int[] arr = null;
// int len = arr.length; // NPE!
// Cause 3: Unboxing null wrapper
Integer num = null;
// int n = num; // NPE — auto-unboxing null Integer
// Cause 4: Return value not checked
String result = findName("Z");
// System.out.println(result.toUpperCase()); // NPE if result is null
// ---- Prevention Techniques ----
// 1. Null check before use
if (result != null) {
System.out.println(result.toUpperCase());
} else {
System.out.println("Name not found");
}
// 2. Java 8 Optional — explicit null handling
java.util.Optional<String> opt = java.util.Optional.ofNullable(findName("Alice"));
String upper = opt.map(String::toUpperCase).orElse("NOT FOUND");
System.out.println(upper); // ALICE
// 3. Objects.requireNonNull — fail fast with clear message
try {
String name = java.util.Objects.requireNonNull(findName("Z"),
"Name lookup returned null for key 'Z'");
} catch (NullPointerException e) {
System.out.println("NPE caught: " + e.getMessage());
}
// 4. String comparisons — use constant.equals() to avoid NPE
String userInput = null;
// if (userInput.equals("admin")) // NPE
if ("admin".equals(userInput)) { // safe — no NPE
System.out.println("Admin!");
} else {
System.out.println("Not admin (safe comparison)");
}
// 5. Java 14+ Helpful NPE messages show exactly what is null
// e.g.: Cannot invoke "String.length()" because "s" is null
}
static String findName(String key) {
java.util.Map<String, String> map = new java.util.HashMap<>();
map.put("Alice", "Alice Smith");
map.put("Bob", "Bob Jones");
return map.get(key); // returns null if key not found
}
}
ALICE
NPE caught: Name lookup returned null for key 'Z'
Not admin (safe comparison)
Java 14+ Helpful NPEs: Since Java 14 (JEP 358),
NPE messages explicitly state what was null, e.g. "Cannot invoke
String.length() because variable 's' is null". Enable with
-XX:+ShowCodeDetailsInExceptionMessages
(default on from Java 17).
11. Exception Handling with Method Overriding
Java enforces strict rules when a subclass overrides a method that declares checked exceptions. The overriding method cannot widen the checked exception clause — it can only narrow it or remove it.
import java.io.*;
public class Parent {
// Declares a checked exception
public void readData() throws IOException {
System.out.println("Parent.readData()");
}
}
public class Child extends Parent {
// ALLOWED: Override with no exception (narrowing — removing)
// @Override
// public void readData() { ... }
// ALLOWED: Override with same exception
@Override
public void readData() throws IOException {
System.out.println("Child.readData() - same exception");
}
// ALLOWED: Override with subclass exception (narrowing)
// @Override
// public void readData() throws FileNotFoundException { ... }
// (FileNotFoundException extends IOException — more specific)
// NOT ALLOWED: Broaden to parent exception
// @Override
// public void readData() throws Exception { ... }
// COMPILE ERROR: Exception is broader than IOException
// NOT ALLOWED: Add unrelated checked exception
// @Override
// public void readData() throws SQLException { ... }
// COMPILE ERROR: SQLException not declared in parent
}
// Unchecked exceptions have NO restrictions in overriding
class Base {
public void process() {
System.out.println("Base.process()");
}
}
class Derived extends Base {
@Override
public void process() throws RuntimeException { // fine — unchecked
System.out.println("Derived.process()");
throw new IllegalStateException("Not ready");
}
}
public class Main {
public static void main(String[] args) {
try {
Parent p = new Child();
p.readData(); // polymorphic call
} catch (IOException e) {
System.out.println("IOException: " + e.getMessage());
}
try {
Base b = new Derived();
b.process(); // throws unchecked at runtime
} catch (RuntimeException e) {
System.out.println("RuntimeException: " + e.getMessage());
}
}
}
RuntimeException: Not ready
| Scenario | Allowed? |
|---|---|
| Override with no checked exception | Yes |
| Override with the same checked exception | Yes |
| Override with a more specific (child) checked exception | Yes |
| Override with a broader (parent) checked exception | No — compile error |
| Override with an unrelated checked exception | No — compile error |
| Override with any unchecked (Runtime) exception | Yes — no restrictions |
12. try-with-resources (Java 7+)
Introduced in Java 7,
try-with-resources automatically closes resources
declared in the parentheses of the
try statement. The resource must
implement java.lang.AutoCloseable (or
its subinterface Closeable). This is
cleaner and safer than using a
finally block.
import java.io.*;
import java.util.Scanner;
public class TryWithResourcesDemo {
// AutoCloseable custom resource
static class DatabaseConnection implements AutoCloseable {
String url;
public DatabaseConnection(String url) {
this.url = url;
System.out.println("Connection opened: " + url);
}
public void query(String sql) {
System.out.println("Executing: " + sql);
}
@Override
public void close() {
System.out.println("Connection closed: " + url);
// In real code: connection.close();
}
}
// Single resource
public static void singleResource() {
try (DatabaseConnection conn = new DatabaseConnection("jdbc:mysql://localhost/db")) {
conn.query("SELECT * FROM users");
conn.query("SELECT * FROM orders");
} // close() called automatically, even if exception occurs
System.out.println("After try block");
}
// Multiple resources — closed in REVERSE order of declaration
public static void multipleResources() {
try (DatabaseConnection conn1 = new DatabaseConnection("db1");
DatabaseConnection conn2 = new DatabaseConnection("db2")) {
conn1.query("SELECT ...");
conn2.query("INSERT ...");
} // conn2 closed first, then conn1
}
// Real-world: reading a file
public static void readFileLines(String path) {
try (BufferedReader br = new BufferedReader(new FileReader(path))) {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (FileNotFoundException e) {
System.out.println("File not found: " + path);
} catch (IOException e) {
System.out.println("IO error: " + e.getMessage());
}
// br.close() called automatically — no finally needed
}
// try-with-resources + catch + finally all together
public static void fullExample(String path) {
try (BufferedReader reader = new BufferedReader(new FileReader(path))) {
System.out.println("First line: " + reader.readLine());
} catch (FileNotFoundException e) {
System.out.println("File missing: " + e.getMessage());
} catch (IOException e) {
System.out.println("Read error: " + e.getMessage());
} finally {
System.out.println("finally still runs with try-with-resources");
}
}
public static void main(String[] args) {
singleResource();
System.out.println("---");
multipleResources();
System.out.println("---");
readFileLines("missing.txt");
System.out.println("---");
fullExample("missing.txt");
}
}
Executing: SELECT * FROM users
Executing: SELECT * FROM orders
Connection closed: jdbc:mysql://localhost/db
After try block
---
Connection opened: db1
Connection opened: db2
Executing: SELECT ...
Executing: INSERT ...
Connection closed: db2
Connection closed: db1
---
File not found: missing.txt
---
File missing: missing.txt (No such file or directory)
finally still runs with try-with-resources
Suppressed Exceptions: If both the try block
and the close() method throw
exceptions, the close exception is suppressed (attached
to the primary exception). You can access them via
e.getSuppressed(). This is one of
the key advantages over manual
finally blocks, which would silently
discard the original exception.
