Skip to main content
Java beginner Lesson 18 of 58

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

ExceptionTypical cause
NullPointerExceptionCalling a method on a null reference
ArrayIndexOutOfBoundsExceptionAccessing arr[-1] or arr[arr.length]
ClassCastException(String) someObject when it’s not a String
IllegalArgumentExceptionBad parameter passed to a method
IllegalStateExceptionMethod called at wrong time (e.g., iterator exhausted)
NumberFormatExceptionInteger.parseInt("abc")
StackOverflowErrorInfinite 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
}

Frequently Asked Questions

What is the difference between checked and unchecked exceptions?
Checked exceptions (IOException, SQLException, etc.) must be declared with 'throws' or caught — the compiler enforces this. Unchecked exceptions extend RuntimeException (NullPointerException, IllegalArgumentException, etc.) and do not need to be declared. Use checked exceptions for recoverable conditions the caller should handle; use unchecked for programming errors.
When should I catch Exception vs a specific exception type?
Always catch the most specific type you can handle. Catching generic Exception can hide bugs and makes it hard to know what actually went wrong. The only valid reason to catch Exception broadly is at a top-level boundary (e.g., a web request handler or main()) to log and return a clean error response.
Does finally always run?
Almost always. The finally block runs even if an exception is thrown, even if there is a return in the try block. The only exceptions are: System.exit() is called, the JVM crashes, or the thread is killed. In Java 7+ prefer try-with-resources over finally for closing resources.
What is the difference between throw and throws?
'throw' is used inside a method body to actually throw an exception instance: throw new IllegalArgumentException('bad input'). 'throws' is part of the method signature to declare that the method may throw a checked exception: public void readFile() throws IOException.