☕ Java

Checked Exception

A checked exception is an exception that the Java compiler requires you to either handle with a try-catch block or declare with a throws clause on the method signature. Checked exceptions represent conditions that are anticipated — failures that a well-written caller should explicitly consider and either recover from or propagate. They extend Exception but not RuntimeException. All checked exceptions in Java's standard library model situations outside the program's control: file not found, network connection refused, malformed data, interrupted thread.

What Makes an Exception Checked

The Java compiler enforces a rule called the catch-or-specify requirement for checked exceptions. If a method calls code that throws a checked exception, the method must either surround the call with a try-catch block that handles the exception, or declare the exception in its own throws clause, propagating responsibility to its caller. Violating this requirement is a compile error — the program will not compile until every checked exception is handled or declared. This requirement is the defining characteristic of checked exceptions and the source of both their value and their controversy. The value is that the compiler forces every programmer to consciously acknowledge that a failure can occur. When you call FileInputStream("data.txt"), the compiler makes you stop and think: what happens if this file does not exist? You must either handle the FileNotFoundException or explicitly state that your method also throws it. The compiler will not let you ignore the possibility. The controversy is that the requirement adds verbosity. Deep call chains where no layer can meaningfully handle the exception end up with throws clauses on every method, passing the responsibility upward until something can handle it. This can make code feel bureaucratic and cluttered, which is why many modern Java frameworks and libraries have moved toward unchecked exceptions for API design. Whether a specific exception should be checked or unchecked is fundamentally a question about the expected caller and their ability to respond. If the caller is expected to have a recovery strategy when this failure occurs — reading from a backup file, retrying with a different server, asking the user to correct their input — then the exception should be checked. If the failure represents a programming error or a condition where recovery is generally not expected, it should be unchecked.
Java
// ── Compiler enforces handling for checked exceptions: ────────────────

// This will NOT compile — IOException is checked and must be handled:
public void readFile() {
    FileReader reader = new FileReader("data.txt");  // COMPILE ERROR
    // java: unreported exception java.io.FileNotFoundException;
    // must be caught or declared to be thrown
}

// ── Option 1: Handle with try-catch: ─────────────────────────────────
public void readFile() {
    try {
        FileReader reader = new FileReader("data.txt");
        // read file...
    } catch (FileNotFoundException e) {
        System.err.println("File not found: " + e.getMessage());
        // recovery logic: use default values, log, alert user, etc.
    }
}

// ── Option 2: Declare with throws — propagate to caller: ─────────────
public void readFile() throws FileNotFoundException {
    FileReader reader = new FileReader("data.txt");
    // This method shifts responsibility to its caller.
    // The compiler now requires CALLERS of readFile() to handle or declare it.
}

// ── Propagation chain: ────────────────────────────────────────────────
// If readFile() declares throws FileNotFoundException:
public void processData() throws FileNotFoundException {
    readFile();          // propagates — declares too
}

public void run() throws FileNotFoundException {
    processData();       // propagates again
}

public static void main(String[] args) {
    try {
        new App().run(); // must eventually be caught
    } catch (FileNotFoundException e) {
        System.err.println("Cannot start: " + e.getMessage());
    }
}

// ── Checked exceptions extend Exception (not RuntimeException): ───────
IOException          ioe = new IOException("IO failed");
FileNotFoundException fnf = new FileNotFoundException("file.txt");
SQLException         sqe = new SQLException("DB error");
ParseException       pe  = new ParseException("parse failed", 0);

// All are checked — compiler requires handling:
System.out.println(ioe instanceof Exception);     // true
System.out.println(ioe instanceof RuntimeException); // false — checked!

Common Checked Exceptions in Java

Java's standard library uses checked exceptions for I/O operations, networking, SQL, parsing, and inter-thread communication — all situations where external factors outside the program's control can cause failures. Understanding which standard exceptions are checked tells you which API calls require try-catch or throws declarations. IOException and its subclasses are the most frequently encountered checked exceptions. FileNotFoundException is thrown when a file path does not refer to an existing file. IOException is thrown for general I/O failures — stream errors, closed streams, broken pipes. InterruptedException is thrown when a thread waiting in sleep(), wait(), or join() is interrupted by another thread — this is a critical exception in concurrent code that should never be silently swallowed. SQLException is thrown for database operation failures. ParseException is thrown when data cannot be parsed from its text representation. ClassNotFoundException is thrown by reflection when a class cannot be found by name. The multi-catch syntax (Java 7+) allows handling multiple checked exception types in a single catch block when the handling logic is the same. The alternative catch types are separated by | and the caught variable is implicitly final. This reduces boilerplate when you have the same recovery action for different failure types — for example, logging an error and returning a default value whether the failure was an IOException or a SQLException.
Java
// ── Common checked exceptions and where they come from: ─────────────

// IOException and subclasses — file and stream operations:
try {
    FileReader   reader = new FileReader("data.txt");     // FileNotFoundException
    BufferedReader br   = new BufferedReader(reader);
    String line         = br.readLine();                   // IOException
    br.close();                                            // IOException
} catch (FileNotFoundException e) {
    System.err.println("File does not exist: " + e.getMessage());
} catch (IOException e) {
    System.err.println("IO error: " + e.getMessage());
}

// InterruptedException — thread operations:
try {
    Thread.sleep(1000);           // InterruptedException
    Object lock = new Object();
    synchronized(lock) {
        lock.wait();              // InterruptedException
    }
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();  // restore interrupt flag — critical!
    System.out.println("Thread was interrupted");
}

// SQLException — database operations:
try {
    Connection conn   = DriverManager.getConnection(url, user, pass);
    PreparedStatement ps = conn.prepareStatement("SELECT * FROM users");
    ResultSet rs      = ps.executeQuery();
    while (rs.next()) {
        System.out.println(rs.getString("name"));
    }
} catch (SQLException e) {
    System.err.println("Database error: " + e.getMessage());
}

// ParseException — date/number parsing:
try {
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
    Date date = sdf.parse("2025-05-30");    // ParseException
} catch (ParseException e) {
    System.err.println("Date parse error at position " + e.getErrorOffset());
}

// ClassNotFoundException — reflection:
try {
    Class<?> cls = Class.forName("com.example.SomeClass"); // ClassNotFoundException
} catch (ClassNotFoundException e) {
    System.err.println("Class not found: " + e.getMessage());
}

// ── Multi-catch — same handling for different exceptions: ─────────────
try {
    Connection conn = DriverManager.getConnection(url);
    FileReader  fr  = new FileReader("config.txt");
} catch (SQLException | IOException e) {   // multi-catch
    System.err.println("Initialisation failed: " + e.getMessage());
    // e is implicitly final in multi-catch
}

Checked Exception Best Practices

Checked exceptions are powerful but easy to misuse. The most serious misuse is swallowing exceptions — catching a checked exception and doing nothing with it. An empty catch block silently hides a failure, making debugging extremely difficult because there is no indication that something went wrong. At minimum, log the exception; even better, take appropriate recovery action or rethrow. Exception translation means catching a low-level checked exception and rethrowing it as a higher-level exception more appropriate for the current abstraction. When a database layer method throws SQLException for a missing record, the service layer should not propagate that database-specific exception to the presentation layer. Instead, it should catch SQLException and throw a domain-level exception like RecordNotFoundException. This keeps each layer's exception vocabulary appropriate for its abstraction level. Always preserve the original as the cause. Declaring throws Exception (the base checked exception class) in a method signature is generally poor practice — it forces callers to catch Exception, which is too broad and hides what can actually go wrong. Declare the specific exceptions that can actually be thrown. Similarly, catching Exception rather than specific types catches too broadly — you might accidentally catch and mishandle a NullPointerException or other unchecked exception that indicates a programming error. The throws clause on a method is part of its API contract. Removing a throws declaration from an existing method is a binary-compatible change (callers no longer need to handle it), but adding one to a public method is a breaking change for checked exceptions — existing callers will no longer compile. This is one argument for unchecked exceptions in library APIs where the API must evolve.
Java
// ── NEVER swallow exceptions silently: ───────────────────────────────
// BAD — exception silently ignored, failure undetectable:
try {
    config = loadConfig("settings.properties");
} catch (IOException e) {
    // Do nothing — this hides the failure completely
}

// SLIGHTLY BETTER — at least log it:
try {
    config = loadConfig("settings.properties");
} catch (IOException e) {
    logger.error("Failed to load config: {}", e.getMessage(), e);
    config = Config.defaults();    // use fallback
}

// ── Exception translation — convert to appropriate abstraction: ────────
// BAD — exposes SQL details to service layer:
public User findUser(String id) throws SQLException {
    return database.query("SELECT * FROM users WHERE id = ?", id);
}

// GOOD — translate to domain exception, preserve cause:
public User findUser(String id) throws UserNotFoundException {
    try {
        User user = database.query("SELECT * FROM users WHERE id = ?", id);
        if (user == null) throw new UserNotFoundException(id);
        return user;
    } catch (SQLException e) {
        throw new UserNotFoundException(id, e);  // preserve cause
    }
}

// ── Do not declare throws Exception or catch Exception broadly: ────────
// BAD — hides what can actually go wrong:
public void processOrder(Order order) throws Exception {
    validateOrder(order);    // throws ValidationException
    chargeCustomer(order);   // throws PaymentException
    fulfillOrder(order);     // throws FulfillmentException
}

// GOOD — declare specific exceptions:
public void processOrder(Order order)
        throws ValidationException, PaymentException, FulfillmentException {
    validateOrder(order);
    chargeCustomer(order);
    fulfillOrder(order);
}

// ── Restore interrupt status when catching InterruptedException: ───────
// WRONG — swallows interrupt, thread never knows it was interrupted:
try {
    Thread.sleep(5000);
} catch (InterruptedException e) {
    // Do nothing — VERY BAD in thread management
}

// CORRECT — restore interrupt flag so callers can check it:
try {
    Thread.sleep(5000);
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();   // restore interrupted status
    // Now clean up and return, or propagate
    return;
}

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.