Java Tutorial
🔍

Java Checked vs Unchecked Exceptions

Java Checked vs Unchecked Exceptions

Java is the only major mainstream language that distinguishes between exceptions the compiler forces you to handle and exceptions it does not. That distinction — checked versus unchecked — is not a stylistic choice. It is a design contract built into the language: checked exceptions say "this failure is part of this method's normal operation contract, and every caller must have a plan." Unchecked exceptions say "this is a programming mistake or system failure — handle it at the boundary, not everywhere."

Getting this distinction wrong leads to two common problems: throws declarations that spread across an entire codebase for exceptions nobody can recover from, and silent swallowing of exceptions because catching them was made mandatory and a blank catch block was the path of least resistance.

What Are Checked and Unchecked Exceptions?

The classification comes purely from position in the class hierarchy. The Java compiler checks this position when compiling every method call.

CLASSIFICATION RULE — based on class hierarchy:

  Extends RuntimeException (directly or transitively)?
    YES → UNCHECKED — compiler does NOT require handling or declaration
    NO, but extends Exception?
    YES → CHECKED   — compiler REQUIRES: catch it OR declare throws

  Extends Error?
    → Also unchecked — compiler does not require handling

java.lang.Throwable
    |
    +── Error                   UNCHECKED (JVM failures — almost never catch)
    |
    +── Exception
            |
            +── RuntimeException    UNCHECKED (programming errors — fix the code)
            |       NullPointerException
            |       IllegalArgumentException
            |       IndexOutOfBoundsException
            |       ClassCastException
            |       UnsupportedOperationException
            |       ConcurrentModificationException
            |
            +── IOException         CHECKED (caller must handle or declare)
            +── SQLException        CHECKED
            +── ClassNotFoundException  CHECKED
            +── InterruptedException    CHECKED

THE COMPILER RULE IN PRACTICE:
  If you call a method that declares:    throws IOException
  The compiler requires you to either:

  OPTION A — handle it:
    try { readFile(); } catch (IOException e) { ... }

  OPTION B — propagate it:
    public void myMethod() throws IOException { readFile(); }

  Any other approach:    COMPILE ERROR

Basic Overview — Side-by-Side

FEATURE              CHECKED                          UNCHECKED
────────────────────────────────────────────────────────────────────────
Hierarchy root       Exception (not via RuntimeEx.)   RuntimeException
Compiler enforcement REQUIRED: catch or declare throws No enforcement
Typical cause        Expected external failure        Programming error
                     File missing, DB down, timeout   Null pointer, bad arg
Caller must handle?  Enforced by compiler             No — optional
Propagation          Must be declared in throws        Propagates silently
                     on every method in the chain
Examples — JDK       IOException, SQLException,       NullPointerException,
                     ClassNotFoundException,           IllegalArgumentException,
                     InterruptedException             ClassCastException,
                                                      IndexOutOfBoundsException
Custom class extends Exception (not RuntimeException) RuntimeException
Recovery expectation Caller is expected to recover    Caller should fix the code
                     or explicitly handle the case    or handle at the boundary

WHAT CHECKED EXCEPTIONS SIGNAL:
  "I cannot guarantee this operation succeeds — the file might be missing,
   the network might be down. You (the caller) must decide what to do."

WHAT UNCHECKED EXCEPTIONS SIGNAL:
  "Something is wrong with how this code is being used — fix the caller,
   or catch this at the outermost boundary for error reporting."

Checked Exceptions — Compiler Enforcement in Action

When a method declares a checked exception in its throws clause, the compiler traces every call site and verifies that each one either handles or propagates the exception. This creates a visible contract across the API boundary.

// File: CheckedExceptionDemo.java import java.io.FileNotFoundException; import java.io.IOException; import java.sql.SQLException; public class CheckedExceptionDemo { // ---- Checked exception propagation chain ---- // Level 3 — throws checked exception: callers must handle or propagate static String readConfigFile(String path) throws IOException { if (path == null) throw new IOException("Path cannot be null"); if (path.endsWith(".missing")) throw new FileNotFoundException("Not found: " + path); return "config-content-from-" + path; } // Level 2 — propagates the checked exception: must declare it too static String loadAppConfig(String configPath) throws IOException { // Compiler requires either catch(IOException) or throws IOException here return readConfigFile(configPath); // propagates — no catch block } // Level 1 — handles it: catches IOException, does not propagate further static void initializeApp(String configPath) { try { String content = loadAppConfig(configPath); // throws IOException System.out.println("App initialized with: " + content); } catch (FileNotFoundException fnfe) { // FileNotFoundException is more specific — handle it first System.out.println("Config missing — using defaults: " + fnfe.getMessage()); } catch (IOException ioe) { // Catches remaining IOException subclasses System.out.println("Config load failed — retrying: " + ioe.getMessage()); } } // ---- Multi-catch for unrelated checked exceptions ---- static void demonstrateMultiCatch(String input) { try { if (input.startsWith("db:")) { throw new SQLException("DB error for: " + input); } throw new IOException("IO error for: " + input); } catch (IOException | SQLException combinedException) { // Both exceptions handled identically — multi-catch since Java 7 // The variable is implicitly final in multi-catch System.out.println("Infra failure [" + combinedException.getClass().getSimpleName() + "]: " + combinedException.getMessage()); } } public static void main(String[] args) { System.out.println("=== Checked exception propagation chain ==="); initializeApp("app.properties"); initializeApp("app.missing"); initializeApp(null); System.out.println(); System.out.println("=== Multi-catch with unrelated checked exceptions ==="); demonstrateMultiCatch("file:/etc/config"); demonstrateMultiCatch("db:users"); } }
Output:
=== Checked exception propagation chain ===
App initialized with: config-content-from-app.properties
Config missing — using defaults: Not found: app.missing
Config load failed — retrying: Path cannot be null

=== Multi-catch with unrelated checked exceptions ===
Infra failure [IOException]: IO error for: file:/etc/config
Infra failure [SQLException]: DB error for: db:users

Unchecked Exceptions — No Compiler Enforcement

Unchecked exceptions extend RuntimeException. The compiler does not require handling or declaration. They propagate silently through the call stack until they hit a catch block or terminate the thread.

// File: UncheckedExceptionDemo.java import java.util.List; import java.util.Map; public class UncheckedExceptionDemo { // NullPointerException — most common unchecked exception // Not declared in method signature — propagates silently if not caught static int getCartItemCount(Map<String, List<String>> userCarts, String userId) { // NPE if userCarts is null (do not catch — fix the caller) // Returns 0 if userId has no cart (intentional empty list) return userCarts.getOrDefault(userId, List.of()).size(); } // IllegalArgumentException — contract violation by the caller static double applyDiscount(double price, double discountPercent) { if (price < 0) { throw new IllegalArgumentException( "Price cannot be negative: " + price); } if (discountPercent < 0 || discountPercent > 100) { throw new IllegalArgumentException( "Discount must be 0-100, got: " + discountPercent); } return price * (1.0 - discountPercent / 100.0); } // IndexOutOfBoundsException — programming error: index beyond list size static String getTopRecommendation(List<String> recommendations) { if (recommendations == null || recommendations.isEmpty()) { return "No recommendations available"; } return recommendations.get(0); // safe — null/empty checked above } // Showing how unchecked propagates without declaration static void methodA() { methodB(); // no throws clause needed — IllegalStateException is unchecked } static void methodB() { methodC(); // no throws clause needed } static void methodC() { throw new IllegalStateException("System not ready"); // propagates up silently } public static void main(String[] args) { System.out.println("=== IllegalArgumentException — contract violation ==="); System.out.printf("Rs.1299 after 20%% off: Rs.%.2f%n", applyDiscount(1299, 20)); try { applyDiscount(500, 150); // invalid discount } catch (IllegalArgumentException iae) { System.out.println("Bad input at controller boundary: " + iae.getMessage()); } System.out.println(); System.out.println("=== getCartItemCount — null-safe usage ==="); Map<String, List<String>> carts = Map.of( "user-priya", List.of("item1","item2","item3"), "user-rohan", List.of("item4") ); System.out.println("Priya's cart: " + getCartItemCount(carts, "user-priya")); System.out.println("Rohan's cart: " + getCartItemCount(carts, "user-rohan")); System.out.println("Ananya's cart: " + getCartItemCount(carts, "user-ananya")); System.out.println(); System.out.println("=== Silent propagation without throws declarations ==="); try { methodA(); // IllegalStateException propagates A → B → C without any throws clause } catch (IllegalStateException ise) { System.out.println("Caught at boundary: " + ise.getMessage()); } } }
Output:
=== IllegalArgumentException — contract violation ===
Rs.1299 after 20% off: Rs.1039.20
Bad input at controller boundary: Discount must be 0-100, got: 150.0

=== getCartItemCount — null-safe usage ===
Priya's cart:  3
Rohan's cart:  1
Ananya's cart: 0

=== Silent propagation without throws declarations ===
Caught at boundary: System not ready

The throws Pollution Problem

This is the central argument in every "checked vs unchecked" debate in Java. When a checked exception propagates through a deep call stack, every method in the chain must declare it. If those intermediate methods cannot meaningfully handle the exception — they are just passing it through — the throws declaration adds noise without value.

// File: ThrowsPollutionDemo.java import java.io.IOException; public class ThrowsPollutionDemo { // ---- THE POLLUTION PROBLEM ---- // An IOException from a DAO layer propagates through service and controller layers. // Neither the service nor the controller can do anything useful with it — // they just re-declare it to satisfy the compiler. // DAO layer — originates the checked exception static String fetchFromDatabase(String query) throws IOException { // IOException is checked — must be declared if (query == null) throw new IOException("Null query"); return "result-for-" + query; } // Service layer — cannot handle IOException, just passes it through // Now this method carries throws IOException even though it has nothing useful to say about it static String getUserProfile(String userId) throws IOException { return fetchFromDatabase("SELECT * FROM users WHERE id=" + userId); } // Controller layer — same problem, forced to declare or catch static String handleProfileRequest(String userId) throws IOException { return getUserProfile(userId); } // ---- THE SOLUTION: exception translation ---- // Wrap the checked exception in an unchecked domain exception at the DAO boundary. // Upper layers deal with a meaningful domain exception without carrying IOException. static class ProfileNotFoundException extends RuntimeException { ProfileNotFoundException(String userId, Throwable cause) { super("Profile not found for user: " + userId, cause); } } static String fetchFromDatabaseClean(String query) throws IOException { if (query == null) throw new IOException("Null query"); return "result-for-" + query; } // Translate at the DAO boundary — checked IOException → unchecked ProfileNotFoundException static String getUserProfileClean(String userId) { try { return fetchFromDatabaseClean("SELECT * FROM users WHERE id=" + userId); } catch (IOException ioException) { // Translate: wrap and rethrow as unchecked — IOException is the cause (preserved) throw new ProfileNotFoundException(userId, ioException); } } // No throws declaration needed — cleaner signature static String handleProfileRequestClean(String userId) { return getUserProfileClean(userId); // clean — no checked exception propagation } public static void main(String[] args) { System.out.println("=== With throws pollution: every layer must declare ==="); try { String result = handleProfileRequest("user-001"); System.out.println("Profile: " + result); } catch (IOException ioException) { System.out.println("IOException at controller: " + ioException.getMessage()); } System.out.println(); System.out.println("=== With exception translation: clean signatures ==="); try { String result = handleProfileRequestClean("user-001"); System.out.println("Profile: " + result); handleProfileRequestClean(null); // triggers translation } catch (ProfileNotFoundException pnfe) { System.out.println("Domain exception caught: " + pnfe.getMessage()); System.out.println("Root cause preserved: " + pnfe.getCause().getClass().getSimpleName() + " — " + pnfe.getCause().getMessage()); } } }
Output:
=== With throws pollution: every layer must declare ===
Profile: result-for-SELECT * FROM users WHERE id=user-001

=== With exception translation: clean signatures ===
Profile: result-for-SELECT * FROM users WHERE id=user-001
Domain exception caught: Profile not found for user: null
Root cause preserved: IOException — Null query

When to Use Each — The Decision Framework

This is the most practically important section for interviews and real design work. The decision between checked and unchecked is a design statement about who is responsible for handling the failure.

USE CHECKED EXCEPTION WHEN:

  1. The failure is expected during normal operation
     — A file the user specified might not exist
     — A network call might fail due to a transient outage
     — A database record might not be found (controversial — see below)

  2. The caller can and should have a recovery strategy
     — Show user a fallback option
     — Retry with exponential backoff
     — Return a default value

  3. The method is part of a public library or framework API
     — Callers you do not control must know about the failure mode
     — It belongs to the API contract documentation

  EXAMPLES: IOException, SQLException, ClassNotFoundException,
            InterruptedException, your own FileParseException,
            your own InsufficientFundsException (when overdraft handling is required)

USE UNCHECKED EXCEPTION WHEN:

  1. The failure indicates a programming mistake
     — Caller passed null where non-null was required
     — Caller passed an invalid argument (negative price, empty ID)
     — An object was used before initialization

  2. The failure is so unlikely or unrecoverable that handling it everywhere is useless
     — System configuration is invalid at startup
     — A required service is unavailable (fail fast)

  3. Intermediate layers have no useful response and would just propagate it
     — Avoids throws pollution through service/controller layers
     — Exception translation: wrap checked → unchecked at the DAO/adapter boundary

  4. You are writing code where callers rarely if ever recover
     — Most web framework exception handlers work this way
     — Spring's DataAccessException family: all unchecked

  EXAMPLES: NullPointerException, IllegalArgumentException, IllegalStateException,
            your own OrderNotFoundException (if handled at controller only),
            your own CatalogConfigurationException, your own PaymentValidationException

THE "NOT FOUND" EXCEPTION DEBATE:
  Should OrderNotFoundException be checked or unchecked?
  Checked:   forces every caller to acknowledge the case — good for library code
             where the caller might not know "not found" is possible
  Unchecked: keeps service signatures clean — the controller handles it globally
             Modern Java frameworks (Spring) use unchecked for most domain exceptions

  Decision guide: if the calling code will almost always catch it at a single
  boundary (a REST controller), make it unchecked. If different callers have
  genuinely different recovery strategies, make it checked.

Basic Example

import java.io.IOException; public class Main { // Checked exception — the compiler forces every caller to catch it or declare it static void readFile(boolean fileExists) throws IOException { if (!fileExists) { throw new IOException("File not found"); } System.out.println("File read successfully"); } // Unchecked exception — no throws declaration required, catching it is optional static int divide(int a, int b) { if (b == 0) { throw new ArithmeticException("Cannot divide by zero"); } return a / b; } public static void main(String[] args) { try { readFile(false); } catch (IOException e) { System.out.println("Checked exception caught: " + e.getMessage()); } try { divide(10, 0); } catch (ArithmeticException e) { System.out.println("Unchecked exception caught: " + e.getMessage()); } } }

Output

Checked exception caught: File not found
Unchecked exception caught: Cannot divide by zero

How it works:

  1. ›readFile declares throws IOException, which is a checked exception — IOException extends Exception but not RuntimeException, so the compiler would refuse to compile main unless the call to readFile was wrapped in try-catch or main also declared throws IOException.
  2. ›divide throws ArithmeticException, which extends RuntimeException — this is unchecked, so the compiler does not require the call to be inside a try-catch at all; it is caught here only because the code chooses to.
  3. ›Both exceptions are caught the same way at runtime — the difference between checked and unchecked is purely a compile-time rule about whether handling is mandatory.

Performance Considerations

Both checked and unchecked exceptions carry the same construction cost — the JVM captures the full stack trace when the exception object is created. There is no performance difference between them at runtime.

COMMON MISCONCEPTION:
  "Unchecked exceptions are faster because the compiler doesn't check them."
  FALSE — compiler enforcement is a compile-time activity.
  At runtime, both exception types:
    — Pay the same stack trace capture cost at construction
    — Unwind the call stack the same way
    — Produce identical diagnostic output

REAL PERFORMANCE CONCERN (applies to BOTH):
  Creating exceptions in tight loops degrades performance.
  A cache miss pattern that throws on every miss is not correct use of exceptions.

  WRONG:
    for (String id : thousandsOfIds) {
        try {
            catalog.findById(id); // throws NotFoundException if absent
        } catch (NotFoundException ignored) {
            defaults.add(id); // exception used as control flow — expensive
        }
    }

  CORRECT:
    for (String id : thousandsOfIds) {
        Optional<Product> product = catalog.findByIdOptional(id);
        product.ifPresentOrElse(
            p -> results.add(p),
            () -> defaults.add(id)); // no exception — just Optional
    }

Best Practices

Use checked exceptions for recoverable failures in public APIs. If you are writing a library or a service layer that other teams call, checked exceptions document the failure modes every caller must plan for. InsufficientBalanceException, InsufficientStockException, QuotaExceededException — these are expected outcomes with specific recovery strategies. Hiding them as unchecked means callers silently ignore conditions they should handle explicitly.

Use unchecked exceptions for programming errors and system failures. NullPointerException, IllegalArgumentException, IllegalStateException signal that the calling code is wrong — not that the system encountered a normal edge case. These should propagate to a boundary handler (a @ControllerAdvice, a thread pool UncaughtExceptionHandler) for logging and generic error responses, not be caught individually at every call site.

Translate checked exceptions at architectural boundaries. A DAO that throws SQLException to a service layer that has no SQL context is leaking implementation details. Translate at the boundary: catch the checked SQLException inside the DAO, wrap it in an unchecked domain exception (InventoryServiceException), and preserve the original as the cause. The service layer deals with a domain concept; the root cause stays in the logs.

Never declare throws Exception or throws Throwable on public methods. These declarations are meaningless — callers cannot do anything useful with them except write their own catch (Exception e) block, which adds no information. Declare the specific checked exception types the method actually throws. If a method genuinely can throw many different exceptions and you have nothing more specific, the method is doing too much.

Common Mistakes

Mistake 1 — Swallowing Checked Exceptions With an Empty Catch Block

// WRONG — catching the checked exception and doing nothing // The program continues as if the file loaded successfully public String loadConfig(String path) { try { return readFile(path); } catch (IOException ioException) { // Empty — exception swallowed // The method returns null, callers get NullPointerException later // The actual cause of the null is completely hidden } return null; } // CORRECT — handle meaningfully or rethrow as a more informative exception public String loadConfig(String path) { try { return readFile(path); } catch (IOException ioException) { // Option 1: return a safe default and log the issue System.err.println("Config not loaded — using defaults: " + ioException.getMessage()); return "DEFAULT_CONFIG"; // Option 2: translate to an unchecked exception preserving the cause // throw new ConfigurationException("Failed to load: " + path, ioException); } }

Mistake 2 — Making Every Domain Exception Checked

// WRONG — three layers forced to carry throws declarations for something // that only the controller handles class OrderNotFoundException extends Exception { // checked OrderNotFoundException(String id) { super("Order not found: " + id); } } // Now every method in the chain must declare it: Order getOrder(String id) throws OrderNotFoundException { ... } OrderSummary summarize(String id) throws OrderNotFoundException { ... } Invoice generateInvoice(String id) throws OrderNotFoundException { ... } // Clutters signatures; none of these intermediate methods do anything with it // CORRECT — make it unchecked if only the controller handles it class OrderNotFoundException extends RuntimeException { // unchecked OrderNotFoundException(String id) { super("Order not found: " + id); } } // Now only the controller catches it — intermediate methods stay clean

Mistake 3 — Wrapping Checked Exceptions Without Preserving the Cause

// WRONG — original cause is lost; root cause disappears from logs public void processOrder(String orderId) { try { database.save(orderId); } catch (java.sql.SQLException sqlException) { throw new OrderProcessingException("Save failed"); // getCause() returns null — the SQL error vanishes } } // CORRECT — always pass the original exception as the second argument public void processOrder(String orderId) { try { database.save(orderId); } catch (java.sql.SQLException sqlException) { throw new OrderProcessingException("Save failed for: " + orderId, sqlException); // Full chain in logs: OrderProcessingException → SQLException → ORA-... error } }

Mistake 4 — Converting Checked to Unchecked by Catching and Ignoring the Type

// WRONG — this converts a checked exception to an unchecked one, // but loses all information about what went wrong public void runTask() { try { riskyOperation(); // throws IOException } catch (IOException ioException) { throw new RuntimeException(ioException.getMessage()); // getCause() is null — the IOException is not preserved as the cause } } // CORRECT — always pass the original as the cause argument public void runTask() { try { riskyOperation(); } catch (IOException ioException) { throw new RuntimeException("Task failed: " + ioException.getMessage(), ioException); // OR: throw new TaskException("Task failed", ioException); // domain exception preferred } }

Interview Questions

Q1. What is the difference between checked and unchecked exceptions in Java?

Checked exceptions extend Exception without going through RuntimeException. The Java compiler enforces handling: any method that calls code declaring throws IOException must either surround the call with try-catch or propagate it with its own throws IOException declaration. Unchecked exceptions extend RuntimeException. The compiler does not require handling or declaration — they propagate silently until caught. Checked exceptions represent expected failures the caller should plan for; unchecked exceptions typically represent programming errors or system failures that should propagate to a global error handler.

Q2. How does the compiler know whether an exception is checked or unchecked?

By inspecting the class hierarchy at compile time. If the exception class extends RuntimeException (directly or through any number of levels), the compiler classifies it as unchecked and does not require handling. If it extends Exception but not RuntimeException, it is checked and the compiler enforces handling or declaration. This is purely a class hierarchy check — there is no keyword, annotation, or other mechanism that marks an exception as checked or unchecked.

Q3. What is throws pollution and how do you solve it?

Throws pollution occurs when a checked exception declared in a low-level method propagates through multiple intermediate layers, forcing every method in the chain to declare throws even though those methods cannot meaningfully handle the exception. A SQLException originating in a DAO propagating through a service layer through a controller without any layer doing anything useful with it is throws pollution. The solution is exception translation: catch the checked exception at the architectural boundary (the DAO layer), wrap it in an unchecked domain exception while preserving the original as the cause, and throw the unchecked exception. Upper layers deal with domain concepts; the root cause is preserved in the exception chain for diagnostic purposes.

Q4. When should you make a custom exception checked versus unchecked?

Use checked when the failure is an expected business condition and different callers have genuinely different recovery strategies — InsufficientFundsException where one caller shows a top-up prompt and another caller sends a payment failure notification. Use unchecked when the failure is handled identically at a single boundary (a REST controller or message handler), when the failure indicates a programming or system error that cannot be recovered from, or when the exception would propagate through multiple layers that have no useful response. Modern Java frameworks like Spring made the pragmatic decision to use unchecked for most database and infrastructure exceptions — DataAccessException is unchecked — specifically to avoid throws pollution.

Q5. What happens when you catch a checked exception and rethrow it as an unchecked exception?

The checked enforcement disappears — callers of the rethrowing method no longer need to handle it. This is the exception translation pattern used at architectural boundaries. The critical requirement: always pass the original exception as the cause argument: throw new ServiceException("message", originalCheckedException). Without this, getCause() returns null and the root cause vanishes from logs. With it, printStackTrace() and logging frameworks show the full chain: ServiceException → IOException → connection refused, which is the diagnostic information that makes production debugging possible.

Q6. Can you convert an unchecked exception to a checked exception?

Yes — by catching the unchecked exception and rethrowing it as a checked exception, or by creating a checked exception class that wraps it. However, this is rarely the right direction. Converting unchecked to checked adds compiler enforcement where there was none, forcing all callers to handle something they were previously unaware of. This is sometimes appropriate when you are writing a library and want to document a failure mode that callers must acknowledge. In application code, the more common and pragmatic flow is checked → unchecked (exception translation at boundaries), not the reverse.

FAQs

Does catching an unchecked exception have any negative effect?

No — catching an unchecked exception works identically to catching a checked one. The only difference is the compiler does not require it. Catching IllegalArgumentException at a REST controller to return a 400 response is correct and common. The concern is catching unchecked exceptions inside business logic as if they represent normal control flow, which hides bugs that should be fixed.

Why does Java have checked exceptions when other languages do not?

Java introduced checked exceptions to make failure modes visible in API contracts. The argument: a method that can throw IOException must declare it, so API documentation does not need to mention it explicitly — the signature itself is the documentation. The counterargument: large-scale experience showed that checked exceptions cause throws pollution and often result in empty catch blocks (swallowing exceptions) because developers found handling them everywhere too burdensome. Modern languages like Kotlin, Scala, and C# dropped checked exceptions entirely. Java never removed them for backward compatibility, but modern Java libraries and frameworks increasingly use unchecked.

What is the difference between throws and throw in the context of checked exceptions?

throw is the statement that creates and dispatches an exception: throw new IOException("message"). throws is the declaration in a method signature that announces checked exceptions a method may propagate: public void readFile() throws IOException. For unchecked exceptions, throw works identically but throws in the signature is optional — you can declare it for documentation purposes, but the compiler does not require it.

If a method throws both a checked and an unchecked exception, how do you declare the signature?

Declare only the checked exceptions: public void save() throws IOException. Unchecked exceptions do not need to appear in the throws clause, though you can add them for documentation clarity — throws IOException, IllegalArgumentException is valid but the IllegalArgumentException part is not enforced by the compiler. IDEs and tools like Javadoc may surface the unchecked ones from the method body regardless.

Can you override a method and add a new checked exception?

No. When overriding a method, the overriding method's throws clause can only declare checked exceptions that are the same as or narrower than those declared in the parent method. You cannot add a new checked exception that the parent did not declare, because callers of the parent type would not know to handle it. You can remove checked exceptions (declare fewer), and you can always declare unchecked exceptions since callers do not need to handle them. This constraint enforces the Liskov Substitution Principle: a subtype must be usable wherever its parent type is expected.

Is InterruptedException checked or unchecked, and how should you handle it?

InterruptedException is a checked exception — it extends Exception directly, not RuntimeException. It is thrown when a thread waiting, sleeping, or blocked is interrupted by another thread calling thread.interrupt(). The correct handling is to either propagate it (throws InterruptedException in the method signature) or, if you must catch it, restore the interrupt flag: catch (InterruptedException e) { Thread.currentThread().interrupt(); }. Never swallow it silently — doing so clears the interrupt flag and the thread no longer responds to future interruption requests, which breaks cancellation and shutdown logic.

Summary

The checked versus unchecked distinction is a compile-time enforcement mechanism that communicates design intent through the class hierarchy. Checked exceptions say "this is an expected, recoverable failure — every caller must decide how to respond." Unchecked exceptions say "this is a programming mistake or system failure — handle it at the boundary."

Two decisions drive most of the design work: whether a custom exception should extend Exception (checked) or RuntimeException (unchecked), and where to apply exception translation — catching checked exceptions at architectural boundaries and rethrowing them as meaningful unchecked domain exceptions while preserving the original cause.

The throws pollution problem explains why modern Java frameworks went unchecked for infrastructure exceptions. The cause-chain preservation rule explains why production debugging is possible at all. Both together are what interviewers at product companies test when they ask "when would you use checked versus unchecked" — they are listening for whether you can explain the design tradeoff, not just name examples.

What to Read Next

Practice Programs

Put this topic into practice — each program has full code, output and a step-by-step explanation you can run online.

Browse all Java programs →