Back to Exception Handling Fundamentals
Q2Question 2 of 4

What is the difference between checked and unchecked exceptions?

Key Concepts
  • Checked exceptions extend Exception but not RuntimeException and the compiler forces you to handle them with try-catch or declare them with throws
  • Unchecked exceptions extend RuntimeException and the compiler does not enforce handling because they represent programming bugs
  • Error extends Throwable directly and represents unrecoverable JVM problems like OutOfMemoryError and StackOverflowError
  • Modern frameworks like Spring wrap checked exceptions in unchecked ones to keep method signatures clean and support lambdas
  • Use checked exceptions only when the immediate caller can take a specific recovery action such as retrying or using a fallback
  • Lambdas and streams cannot throw checked exceptions, which forces ugly try-catch wrappers inside functional pipelines

Definition

Checked exceptions are enforced at compile time and must be caught or declared, while unchecked exceptions extend RuntimeException and represent programming bugs that the compiler does not force you to handle.
  • Checked exceptions extend Exception but not RuntimeException. The compiler forces every caller to either catch them or declare them with throws. Examples: IOException, SQLException, FileNotFoundException.
  • Unchecked exceptions extend RuntimeException. The compiler does not enforce handling. Examples: NullPointerException, IllegalArgumentException, ArrayIndexOutOfBoundsException.
  • Errors extend Error (a sibling of Exception under Throwable). They represent serious JVM problems like OutOfMemoryError and StackOverflowError. You should not catch them.
  • The argument against checked exceptions in modern APIs is that they create boilerplate, pollute method signatures with throws chains, and break lambda expressions in streams.

At National Bank of India, a checked exception is like the mandatory risk disclosure form for international wire transfers. Even if 99 percent of transfers succeed, the bank requires Priya to sign the form before processing. An unchecked exception is like Priya accidentally entering a negative amount on the transfer screen. Nobody forces her to fill out a form for that because it is a user mistake that should be prevented by input validation, not by paperwork.

Understanding the Concepts

What is the exception hierarchy in Java?

Every exception in Java descends from Throwable. The hierarchy splits into two branches: Error for JVM-level problems and Exception for application-level problems. Inside Exception, there is a special sub-branch called RuntimeException for unchecked exceptions.

JAVA
package com.bank.demo;

public class ExceptionHierarchyDemo {
    public static void main(String[] args) {
        // Throwable
        //   ├── Error                          (unchecked, JVM-level)
        //   │     ├── OutOfMemoryError
        //   │     ├── StackOverflowError
        //   │     └── VirtualMachineError
        //   └── Exception                      (checked by default)
        //         ├── IOException               (checked)
        //         ├── SQLException              (checked)
        //         ├── FileNotFoundException     (checked)
        //         └── RuntimeException          (unchecked)
        //               ├── NullPointerException
        //               ├── IllegalArgumentException
        //               ├── IllegalStateException
        //               ├── IndexOutOfBoundsException
        //               └── ClassCastException

        System.out.println("RuntimeException parent: " + RuntimeException.class.getSuperclass().getSimpleName());
        System.out.println("IOException parent: " + java.io.IOException.class.getSuperclass().getSimpleName());
        System.out.println("Error parent: " + Error.class.getSuperclass().getSimpleName());
    }
}
TEXT
RuntimeException parent: Exception
IOException parent: Exception
Error parent: Throwable

The rule is straightforward. If a class extends Exception but does not extend RuntimeException, it is checked. If it extends RuntimeException, it is unchecked. If it extends Error, it is also unchecked but represents a JVM failure you should not catch.

What is a checked exception?

A checked exception is any exception that the compiler forces you to handle. When a method can throw a checked exception, every caller must either wrap the call in a try-catch block or declare the exception in its own throws clause.

JAVA
package com.bank.service;

import java.io.IOException;
import java.math.BigDecimal;
import java.nio.file.Files;
import java.nio.file.Path;

public class TradeFileService {

    // The compiler forces the caller to handle IOException
    public String readTradeFile(String path) throws IOException {
        return Files.readString(Path.of(path));
    }

    public static void main(String[] args) {
        TradeFileService service = new TradeFileService();

        // Option 1: catch the checked exception
        try {
            String data = service.readTradeFile("/trades/daily.csv");
            System.out.println("Loaded " + data.length() + " characters");
        } catch (IOException e) {
            System.err.println("Failed to read trade file: " + e.getMessage());
        }

        // Option 2: declare throws in main (pass to JVM)
        // public static void main(String[] args) throws IOException { ... }
    }
}
TEXT
Failed to read trade file: /trades/daily.csv

If you remove the try-catch and do not add throws IOException to main, the compiler produces an error: Unhandled exception: java.io.IOException.

What is an unchecked exception?

An unchecked exception extends RuntimeException. The compiler does not force the caller to handle it because unchecked exceptions typically represent programming bugs. The fix is to write better code, not to wrap everything in try-catch.

JAVA
package com.bank.service;

import java.math.BigDecimal;

public class AccountService {

    private BigDecimal balance = new BigDecimal("100000.00");

    public void withdraw(BigDecimal amount) {
        if (amount == null) {
            throw new IllegalArgumentException("Amount must not be null");
        }
        if (amount.compareTo(BigDecimal.ZERO) <= 0) {
            throw new IllegalArgumentException("Amount must be positive: " + amount);
        }
        if (amount.compareTo(balance) > 0) {
            throw new IllegalStateException(
                "Insufficient balance: requested " + amount + ", available " + balance);
        }
        balance = balance.subtract(amount);
        System.out.println("Withdrawn Rs." + amount + ", remaining Rs." + balance);
    }

    public static void main(String[] args) {
        AccountService service = new AccountService();
        service.withdraw(new BigDecimal("25000.00"));
        service.withdraw(new BigDecimal("-500.00"));  // throws IllegalArgumentException
    }
}
TEXT
Withdrawn Rs.25000.00, remaining Rs.75000.00
Exception in thread "main" java.lang.IllegalArgumentException: Amount must be positive: -500.00

Notice that the withdraw method does not declare throws IllegalArgumentException. The compiler does not require it because IllegalArgumentException is unchecked.

How is a checked exception different from an unchecked exception?

The core difference is compile-time enforcement. The table below summarizes the key distinctions.

| Feature | Checked Exception | Unchecked Exception |
|---|---|---|
| Extends | Exception (not RuntimeException) | RuntimeException |
| Compiler enforcement | Must catch or declare throws | No enforcement |
| Typical cause | External failure (IO, network, DB) | Programming bug (null, bad cast) |
| Examples | IOException, SQLException | NullPointerException, IllegalArgumentException |
| Recovery | Caller can retry, use fallback | Fix the code, do not catch |
| Method signature | Pollutes with throws chain | Clean signatures |
| Lambda compatible | No (requires wrapper) | Yes (no special handling) |

JAVA
package com.bank.demo;

import java.io.IOException;
import java.math.BigDecimal;

public class CheckedVsUncheckedDemo {

    // Checked: compiler forces handling
    public static String loadConfig() throws IOException {
        throw new IOException("Config file missing");
    }

    // Unchecked: compiler does not force handling
    public static BigDecimal parseAmount(String input) {
        if (input == null || input.isBlank()) {
            throw new IllegalArgumentException("Input must not be blank");
        }
        return new BigDecimal(input);
    }

    public static void main(String[] args) {
        // Checked: MUST handle
        try {
            loadConfig();
        } catch (IOException e) {
            System.out.println("Checked caught: " + e.getMessage());
        }

        // Unchecked: optional to handle, but crashes if not caught
        try {
            parseAmount("");
        } catch (IllegalArgumentException e) {
            System.out.println("Unchecked caught: " + e.getMessage());
        }
    }
}
TEXT
Checked caught: Config file missing
Unchecked caught: Input must not be blank

Why do checked exceptions exist?

Checked exceptions exist because Java's designers wanted the compiler to enforce handling of predictable, recoverable failures. When a method reads a file, the file might not exist. When a method connects to a database, the database might be down. These are not bugs in your code. They are environmental conditions that you should plan for.

The idea is that the compiler acts as a safety net. If you call a method that might fail due to an external resource, the compiler forces you to write recovery logic. This prevents the common mistake of ignoring potential failures.

JAVA
package com.bank.service;

import java.io.FileNotFoundException;
import java.math.BigDecimal;
import java.nio.file.Files;
import java.nio.file.Path;

public class StatementService {

    // Checked exception signals a recoverable situation:
    // the file might not exist, and the caller can try a fallback path
    public String loadStatement(String accountId) throws FileNotFoundException {
        Path primary = Path.of("/statements/" + accountId + ".pdf");
        Path fallback = Path.of("/archive/" + accountId + ".pdf");

        if (Files.exists(primary)) {
            return primary.toString();
        } else if (Files.exists(fallback)) {
            return fallback.toString();
        }
        throw new FileNotFoundException("Statement not found for account " + accountId);
    }

    public static void main(String[] args) {
        StatementService service = new StatementService();
        try {
            String path = service.loadStatement("SAV2001");
            System.out.println("Statement at: " + path);
        } catch (FileNotFoundException e) {
            System.out.println("Generating fresh statement for customer...");
        }
    }
}
TEXT
Generating fresh statement for customer...

What is the argument against checked exceptions in modern APIs?

Modern Java frameworks have largely moved away from checked exceptions. Spring Data wraps SQLException in unchecked DataAccessException. Spring Boot controllers rely on @ControllerAdvice for global handling. The main arguments against checked exceptions are:

  • Boilerplate. Most code just wraps checked exceptions in unchecked ones anyway.
  • Signature pollution. The throws clause propagates up through 5 to 10 methods, forcing every method in the chain to declare it.
  • Lambda incompatibility. Functional interfaces like Function and Consumer do not declare throws, so checked exceptions cannot be thrown inside lambdas without an ugly try-catch wrapper.
JAVA
package com.bank.demo;

import java.math.BigDecimal;
import java.util.List;

public class LambdaExceptionDemo {

    // Simulating a checked exception in a service
    static BigDecimal fetchRate(String currency) throws Exception {
        if ("USD".equals(currency)) return new BigDecimal("83.50");
        throw new Exception("Rate not found for " + currency);
    }

    public static void main(String[] args) {
        List<String> currencies = List.of("USD", "EUR", "GBP");

        // Checked exception inside lambda requires ugly wrapper
        currencies.stream()
            .map(c -> {
                try {
                    return c + " = Rs." + fetchRate(c);
                } catch (Exception e) {
                    return c + " = UNAVAILABLE";
                }
            })
            .forEach(System.out::println);

        // If fetchRate threw unchecked, the lambda would be clean:
        // .map(c -> c + " = Rs." + fetchRateUnchecked(c))
    }
}
TEXT
USD = Rs.83.50
EUR = UNAVAILABLE
GBP = UNAVAILABLE

When should you use checked exceptions?

Use checked exceptions when all three conditions are true: the failure is expected and comes from an external resource, the immediate caller can take a specific recovery action, and the recovery logic differs from caller to caller.

JAVA
package com.bank.service;

import java.io.IOException;
import java.math.BigDecimal;
import java.nio.file.Files;
import java.nio.file.Path;

public class TradeImportService {

    // Checked: caller CAN recover by trying a fallback source
    public String importTrades(String primaryPath) throws IOException {
        return Files.readString(Path.of(primaryPath));
    }

    public static void main(String[] args) {
        TradeImportService service = new TradeImportService();

        // Caller 1: retry with fallback file
        try {
            String data = service.importTrades("/trades/primary.csv");
            System.out.println("Loaded from primary");
        } catch (IOException e) {
            System.out.println("Primary failed, trying fallback...");
            try {
                String data = service.importTrades("/trades/fallback.csv");
                System.out.println("Loaded from fallback");
            } catch (IOException e2) {
                System.err.println("Both sources failed: " + e2.getMessage());
            }
        }
    }
}
TEXT
Primary failed, trying fallback...
Both sources failed: /trades/fallback.csv

When should you NOT use checked exceptions?

Do not use checked exceptions when the failure represents a programming bug, when the caller will just wrap it in an unchecked exception anyway, or when the exception is used inside lambdas and streams.

JAVA
package com.bank.demo;

import java.math.BigDecimal;

public class UncheckedPreferenceDemo {

    // WRONG: checked exception for a programming bug
    // public void deposit(BigDecimal amount) throws NegativeAmountException

    // CORRECT: unchecked exception for invalid input
    public void deposit(BigDecimal amount) {
        if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
            throw new IllegalArgumentException("Deposit amount must be positive: " + amount);
        }
        System.out.println("Deposited Rs." + amount);
    }

    public static void main(String[] args) {
        UncheckedPreferenceDemo demo = new UncheckedPreferenceDemo();
        demo.deposit(new BigDecimal("5000.00"));

        try {
            demo.deposit(new BigDecimal("-100.00"));
        } catch (IllegalArgumentException e) {
            System.out.println("Caught: " + e.getMessage());
        }
    }
}
TEXT
Deposited Rs.5000.00
Caught: Deposit amount must be positive: -100.00

How does Spring handle checked exceptions?

Spring wraps all checked JDBC exceptions in the unchecked DataAccessException hierarchy. This means your service and controller code never sees SQLException. The global exception handler catches domain exceptions and maps them to HTTP responses.

JAVA
package com.bank.demo;

// Spring wraps checked → unchecked:
// SQLException            → DataAccessException (unchecked)
// IOException             → UncategorizedIOException (unchecked)
// ClassNotFoundException  → BeanCreationException (unchecked)

// Your service layer:
// public TradeResult settle(String tradeId) {
//     return tradeRepo.findById(tradeId)          // DataAccessException (unchecked)
//         .orElseThrow(() -> new TradeNotFoundException(tradeId));  // unchecked
// }

// Your controller:
// @PostMapping("/settle/{id}")
// public TradeResult settle(@PathVariable String id) {
//     return tradeService.settle(id);  // ZERO try-catch!
// }

// Global handler catches everything:
// @RestControllerAdvice
// @ExceptionHandler(TradeNotFoundException.class) → 404
// @ExceptionHandler(DataAccessException.class)    → 500

public class SpringExceptionWrapDemo {
    public static void main(String[] args) {
        System.out.println("Spring wraps SQLException → DataAccessException (unchecked)");
        System.out.println("Controllers have ZERO try-catch blocks");
        System.out.println("@ControllerAdvice handles exceptions globally");
    }
}
DIAGRAM
Spring wraps SQLException → DataAccessException (unchecked)
Controllers have ZERO try-catch blocks
@ControllerAdvice handles exceptions globally

What are the common mistakes with checked vs unchecked exceptions?

The most common mistake is wrapping every checked exception in a generic RuntimeException without adding context.

JAVA
package com.bank.demo;

import java.math.BigDecimal;

public class CommonMistakesDemo {

    // WRONG: wrapping without context
    public static void wrongWay() {
        try {
            // Simulate checked exception
            throw new Exception("Connection refused: port 5432");
        } catch (Exception e) {
            throw new RuntimeException(e);  // no message, no context!
        }
    }

    // CORRECT: wrap with domain context and preserve cause
    public static void correctWay() {
        try {
            throw new Exception("Connection refused: port 5432");
        } catch (Exception e) {
            throw new RuntimeException(
                "Failed to settle trade TRD-001 for account SAV2001", e);
        }
    }

    public static void main(String[] args) {
        try {
            wrongWay();
        } catch (RuntimeException e) {
            System.out.println("Wrong: " + e.getMessage());
        }

        try {
            correctWay();
        } catch (RuntimeException e) {
            System.out.println("Correct: " + e.getMessage());
            System.out.println("Root cause: " + e.getCause().getMessage());
        }
    }
}
TEXT
Wrong: java.lang.Exception: Connection refused: port 5432
Correct: Failed to settle trade TRD-001 for account SAV2001
Root cause: Connection refused: port 5432

What are the edge cases?

A few surprising behaviors catch developers off guard.

JAVA
package com.bank.demo;

public class EdgeCasesDemo {

    // Edge case 1: you CAN declare throws for unchecked exceptions
    // It compiles but the compiler does not force callers to handle it
    public static void explicitUnchecked() throws IllegalArgumentException {
        throw new IllegalArgumentException("Explicitly declared but optional");
    }

    // Edge case 2: catch blocks can catch unchecked exceptions
    // even though the compiler does not force it
    public static void catchUnchecked() {
        try {
            String s = null;
            s.length();  // NullPointerException
        } catch (NullPointerException e) {
            System.out.println("Caught NPE: " + e.getMessage());
        }
    }

    // Edge case 3: Error is unchecked but is NOT a RuntimeException
    // Error extends Throwable directly, not Exception
    public static void errorIsNotException() {
        System.out.println("Error extends Throwable: " +
            (Error.class.getSuperclass() == Throwable.class));
        System.out.println("RuntimeException extends Exception: " +
            (RuntimeException.class.getSuperclass() == Exception.class));
    }

    public static void main(String[] args) {
        // Edge case 1
        try {
            explicitUnchecked();
        } catch (IllegalArgumentException e) {
            System.out.println("Edge 1: " + e.getMessage());
        }

        // Edge case 2
        catchUnchecked();

        // Edge case 3
        errorIsNotException();
    }
}
TEXT
Edge 1: Explicitly declared but optional
Caught NPE: Cannot invoke "String.length()" because "s" is null
Error extends Throwable: true
RuntimeException extends Exception: true

What should a senior engineer know?

  • Checked exceptions were a design experiment. Java is one of the few languages that has them. C#, Kotlin, Scala, and Python all use only unchecked exceptions.
  • Spring's philosophy. Spring Data, Spring Boot, and Spring Security convert all checked exceptions to unchecked ones. The reasoning is that most callers cannot meaningfully recover from a SQLException and just rethrow it wrapped.
  • Sneaky throws. Lombok's @SneakyThrows and some library utilities use type erasure to throw checked exceptions without declaring them. This is controversial but widely used in projects that avoid checked exceptions entirely.
  • Project Loom. Virtual threads (Java 21) work naturally with unchecked exceptions. Checked exceptions in structured concurrency require explicit handling at the scope level.
  • Kotlin interop. Kotlin does not have checked exceptions. When calling Java methods that declare throws, Kotlin does not force handling. This is one reason Kotlin chose to drop checked exceptions.

What are common interview follow-up questions?

  • Can you create a custom checked exception? (Yes, extend Exception.)
  • Can you create a custom unchecked exception? (Yes, extend RuntimeException.)
  • What happens if a checked exception is declared in throws but never actually thrown? (It compiles fine but is misleading.)
  • Can a method override narrow the throws clause? (Yes, a subclass can declare fewer or narrower checked exceptions.)
  • Can a method override widen the throws clause? (No, a subclass cannot declare new checked exceptions not in the parent.)

Visual Overview

DIAGRAM
                    Throwable
                    ├───────────────────────┐
                    │                       │
                  Error                  Exception
              (unchecked)            (checked by default)
                    │                       │
         ┌──────────┤              ┌────────┴─────────┐
         │          │              │                   │
    OutOfMemory  StackOverflow  IOException     RuntimeException
      Error        Error        SQLException      (unchecked)
                                                      │
                                             ┌────────┴────────┐
                                             │                  │
                                      NullPointer       IllegalArgument
                                       Exception          Exception

  ┌─────────────────────────────────────────────────────────┐
  │  Checked = Exception minus RuntimeException branch      │
  │  Unchecked = RuntimeException branch + Error branch     │
  └─────────────────────────────────────────────────────────┘

Key Takeaway

Java divides exceptions into two categories based on compile-time enforcement. Checked exceptions extend Exception but not RuntimeException. The compiler forces every caller to catch them or declare them with throws. They represent predictable, recoverable failures from external resources like files, databases, and network connections. Unchecked exceptions extend RuntimeException and the compiler does not enforce handling. They represent programming bugs like null references, invalid arguments, and bad casts. The fix is better code, not more try-catch blocks.

The argument against checked exceptions in modern APIs is practical. Most application code wraps checked exceptions in unchecked ones anyway, method signatures accumulate long throws chains as exceptions propagate upward, and lambdas in streams cannot throw checked exceptions without ugly wrappers. Spring Boot, Kotlin, and most modern frameworks have moved to unchecked exceptions with centralized global handlers.

In a banking service at National Bank of India, Pankaj (the architect) uses checked exceptions only for truly recoverable situations where the immediate caller has a meaningful fallback. A FileNotFoundException when loading a trade file is checked because the caller can try a backup path. An IllegalArgumentException for a negative transfer amount is unchecked because it is a programming bug that the caller should prevent with validation.

Remember one sentence: checked exceptions force compile-time handling for recoverable external failures, while unchecked exceptions signal programming bugs that should be fixed in code rather than caught.

---

Share
Use ← → arrow keys to navigate