Error Types in Java
Understand Java's exception hierarchy — checked vs unchecked exceptions, Errors, finally blocks, and when to use each.
The Exception Hierarchy
Every throwable object in Java extends Throwable. The hierarchy splits into two branches:
Throwable
├── Error (serious JVM problems — do not catch)
│ ├── OutOfMemoryError
│ ├── StackOverflowError
│ └── VirtualMachineError
└── Exception (recoverable conditions)
├── IOException ← checked
├── SQLException ← checked
├── ClassNotFoundException ← checked
└── RuntimeException ← unchecked (no compiler enforcement)
├── NullPointerException
├── ArrayIndexOutOfBoundsException
├── IllegalArgumentException
├── IllegalStateException
├── ClassCastException
└── ArithmeticException
Checked Exceptions
The compiler forces you to handle checked exceptions — you must either catch them or declare them with throws.
import java.io.*;
import java.nio.file.*;
public class CheckedDemo {
// Declares the checked exception — caller must handle it
public static String readFile(String path) throws IOException {
return Files.readString(Path.of(path));
}
public static void main(String[] args) {
// Option 1: catch it here
try {
String content = readFile("config.txt");
System.out.println(content);
} catch (IOException e) {
System.err.println("Could not read file: " + e.getMessage());
}
// Option 2: declare it too (propagate up)
// public static void main(String[] args) throws IOException { ... }
}
}
Catching Multiple Exceptions
import java.io.*;
import java.sql.*;
public class MultiCatch {
public static void loadData(String file) throws IOException, SQLException {
// ...
}
public static void main(String[] args) {
// Catch each type separately — allows different handling
try {
loadData("data.csv");
} catch (IOException e) {
System.err.println("File problem: " + e.getMessage());
} catch (SQLException e) {
System.err.println("DB problem (code " + e.getErrorCode() + "): " + e.getMessage());
}
// Multi-catch — handle multiple types the same way (Java 7+)
try {
loadData("data.csv");
} catch (IOException | SQLException e) {
System.err.println("Data loading failed: " + e.getMessage());
}
}
}
Unchecked Exceptions (RuntimeException)
The compiler does not require you to handle these. They signal programming errors.
public class UncheckedDemo {
// No 'throws' needed — it's unchecked
public static int divide(int a, int b) {
if (b == 0) {
throw new ArithmeticException("Divisor cannot be zero");
}
return a / b;
}
public static String firstChar(String s) {
if (s == null || s.isEmpty()) {
throw new IllegalArgumentException("String must not be null or empty");
}
return String.valueOf(s.charAt(0));
}
public static void main(String[] args) {
// These compile fine without try/catch
System.out.println(divide(10, 2)); // 5
System.out.println(firstChar("Java")); // J
// These throw at runtime:
// divide(10, 0); → ArithmeticException
// firstChar(null); → IllegalArgumentException
// String s = null; s.length(); → NullPointerException
}
}
Common Unchecked Exceptions
| Exception | Typical cause |
|---|---|
NullPointerException | Calling a method on a null reference |
ArrayIndexOutOfBoundsException | Accessing arr[-1] or arr[arr.length] |
ClassCastException | (String) someObject when it’s not a String |
IllegalArgumentException | Bad parameter passed to a method |
IllegalStateException | Method called at wrong time (e.g., iterator exhausted) |
NumberFormatException | Integer.parseInt("abc") |
StackOverflowError | Infinite recursion |
Error — Do Not Catch
Error and its subclasses represent serious JVM-level problems. You should almost never catch these.
// DON'T do this — you can't meaningfully recover from OOM
try {
// ... allocate huge arrays
} catch (OutOfMemoryError e) {
// Too late — JVM state is undefined
}
// StackOverflowError from infinite recursion
static int infinite(int n) {
return infinite(n + 1); // never reaches base case → StackOverflowError
}
The only legitimate catch of an Error is at a top-level boundary (e.g., a server request handler) to log the error before the process dies.
The finally Block
finally always executes — even when an exception is thrown or a return is hit:
public class FinallyDemo {
static String test(boolean throwIt) {
try {
if (throwIt) throw new RuntimeException("oops");
return "try";
} catch (RuntimeException e) {
return "catch";
} finally {
// This ALWAYS runs — even with a return in try/catch
System.out.println("finally ran");
}
}
public static void main(String[] args) {
System.out.println(test(false));
// finally ran
// try
System.out.println(test(true));
// finally ran
// catch
}
}
Prefer try-with-resources Over finally
For any resource that needs closing, use try-with-resources (Java 7+):
import java.io.*;
public class TryWithResources {
// Old way — error-prone finally
static String readOld(String path) throws IOException {
BufferedReader reader = null;
try {
reader = new BufferedReader(new FileReader(path));
return reader.readLine();
} finally {
if (reader != null) reader.close(); // easy to forget or get wrong
}
}
// Modern way — resource closed automatically even if exception is thrown
static String readModern(String path) throws IOException {
try (var reader = new BufferedReader(new FileReader(path))) {
return reader.readLine();
} // reader.close() called here automatically
}
// Multiple resources — closed in reverse order
static void copy(String src, String dst) throws IOException {
try (var in = new FileInputStream(src);
var out = new FileOutputStream(dst)) {
in.transferTo(out);
}
}
}
Custom Exceptions
Create your own exception classes to carry domain-specific context:
// Custom checked exception
public class InsufficientFundsException extends Exception {
private final double amount;
private final double balance;
public InsufficientFundsException(double amount, double balance) {
super(String.format("Cannot withdraw %.2f — balance is %.2f", amount, balance));
this.amount = amount;
this.balance = balance;
}
public double getAmount() { return amount; }
public double getBalance() { return balance; }
}
// Custom unchecked exception
public class InvalidUserException extends RuntimeException {
private final String userId;
public InvalidUserException(String userId) {
super("No user found with id: " + userId);
this.userId = userId;
}
public String getUserId() { return userId; }
}
// Using them
public class BankAccount {
private double balance;
public BankAccount(double balance) { this.balance = balance; }
public void withdraw(double amount) throws InsufficientFundsException {
if (amount > balance) {
throw new InsufficientFundsException(amount, balance);
}
balance -= amount;
}
}
Exception Chaining
Wrap low-level exceptions in higher-level ones to preserve the original cause:
import java.io.*;
import java.sql.*;
public class ExceptionChaining {
public static void loadConfig(String path) throws ConfigException {
try {
String content = Files.readString(Path.of(path));
parseConfig(content);
} catch (IOException e) {
// Wrap with context; original IOException is preserved as 'cause'
throw new ConfigException("Failed to load config from: " + path, e);
}
}
public static class ConfigException extends Exception {
public ConfigException(String message, Throwable cause) {
super(message, cause);
}
}
public static void main(String[] args) {
try {
loadConfig("missing.properties");
} catch (ConfigException e) {
System.err.println(e.getMessage());
System.err.println("Caused by: " + e.getCause().getMessage());
}
}
}
Best Practices Summary
// 1. Catch specific types, not generic Exception
try { ... }
catch (FileNotFoundException e) { /* handle missing file */ }
catch (IOException e) { /* handle other I/O errors */ }
// 2. Don't swallow exceptions silently
try { ... }
catch (Exception e) {
// Bad — bug will go unnoticed
}
// Good — at minimum, log it
catch (Exception e) {
logger.error("Unexpected error", e);
throw new RuntimeException("Operation failed", e);
}
// 3. Use unchecked for programming errors
public void setAge(int age) {
if (age < 0) throw new IllegalArgumentException("Age cannot be negative: " + age);
this.age = age;
}
// 4. Use checked for recoverable I/O / external system failures
public Optional<User> findUser(long id) throws DatabaseException { ... }
// 5. Always use try-with-resources for Closeable resources
try (var conn = dataSource.getConnection()) {
// use conn
}