Definition
- Checked exceptions extend
Exceptionbut notRuntimeException. The compiler forces every caller to either catch them or declare them withthrows. Examples:IOException,SQLException,FileNotFoundException. - Unchecked exceptions extend
RuntimeException. The compiler does not enforce handling. Examples:NullPointerException,IllegalArgumentException,ArrayIndexOutOfBoundsException. - Errors extend
Error(a sibling ofExceptionunderThrowable). They represent serious JVM problems likeOutOfMemoryErrorandStackOverflowError. You should not catch them. - The argument against checked exceptions in modern APIs is that they create boilerplate, pollute method signatures with
throwschains, 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.
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());
}
}
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.
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 { ... }
}
}
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.
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
}
}
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) |
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());
}
}
}
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.
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...");
}
}
}
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
throwsclause propagates up through 5 to 10 methods, forcing every method in the chain to declare it. - Lambda incompatibility. Functional interfaces like
FunctionandConsumerdo not declarethrows, so checked exceptions cannot be thrown inside lambdas without an ugly try-catch wrapper.
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))
}
}
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.
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());
}
}
}
}
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.
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());
}
}
}
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.
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");
}
}
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.
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());
}
}
}
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.
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();
}
}
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
SQLExceptionand just rethrow it wrapped. - Sneaky throws. Lombok's
@SneakyThrowsand 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
throwsbut never actually thrown? (It compiles fine but is misleading.) - Can a method override narrow the
throwsclause? (Yes, a subclass can declare fewer or narrower checked exceptions.) - Can a method override widen the
throwsclause? (No, a subclass cannot declare new checked exceptions not in the parent.)
Visual Overview
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.
---