Home Java Java Checked vs Unchecked Exceptions Explained — When to Use Each

Java Checked vs Unchecked Exceptions Explained — When to Use Each

In Plain English 🔥
Imagine you're booking a flight. The airline knows there's a real chance your preferred seat might be taken, so they force you to acknowledge that before you even finish booking — that's a checked exception, the compiler forces you to deal with a problem that's genuinely likely. An unchecked exception is more like someone trying to divide a restaurant bill by zero people — that's a programming blunder, not something the system should force every caller to prepare for. Checked exceptions say 'this realistic problem will happen, plan for it.' Unchecked exceptions say 'you wrote something wrong, fix your code.'
⚡ Quick Answer
Imagine you're booking a flight. The airline knows there's a real chance your preferred seat might be taken, so they force you to acknowledge that before you even finish booking — that's a checked exception, the compiler forces you to deal with a problem that's genuinely likely. An unchecked exception is more like someone trying to divide a restaurant bill by zero people — that's a programming blunder, not something the system should force every caller to prepare for. Checked exceptions say 'this realistic problem will happen, plan for it.' Unchecked exceptions say 'you wrote something wrong, fix your code.'

Every Java application that touches the outside world — files, databases, networks, APIs — is one bad moment away from something going wrong. The file doesn't exist. The database is down. The network times out. Java's exception system is how your code communicates those failures, but not all failures are created equal. The language designers made a deliberate, architectural choice: split exceptions into two categories with very different rules, and that choice shapes how you design APIs, how you write business logic, and ultimately how maintainable your codebase is.

The problem checked exceptions solve is straightforward: when a method does something risky that the caller absolutely must prepare for, the compiler becomes your teammate and refuses to let you ship code that ignores that risk. Unchecked exceptions solve the opposite problem — if every method that might accidentally receive a null pointer forced every caller to write a try-catch block, Java code would be unreadable noise. The split exists to keep the signal-to-noise ratio sane.

By the end of this article you'll know the exact inheritance hierarchy that separates the two categories, why the language was designed this way, how to write your own custom exceptions correctly, and — most importantly — how to make the judgment call about which type to throw in your own APIs. You'll also see the most common mistakes developers make and how to sidestep them cleanly.

The Inheritance Tree That Controls Everything

Every exception in Java lives inside a class hierarchy, and your position in that tree determines whether the compiler watches you or leaves you alone.

At the top sits Throwable. It has two direct children: Error and Exception. Errors (like OutOfMemoryError) represent JVM-level catastrophes you can't reasonably recover from — ignore them for now. Everything we care about lives under Exception.

Here's the rule that governs everything: any class that extends Exception directly is a checked exception. Any class that extends RuntimeException — which itself extends Exception — is an unchecked exception.

That's the whole rule. There's no annotation, no keyword. It's purely about which class you extend.

RuntimeException was introduced because the designers recognised a class of bugs — null dereferences, bad array indices, illegal arguments — that are caused by programmer mistakes rather than environmental conditions. Wrapping those in try-catch blocks would punish correct code for the sins of incorrect code elsewhere. Checked exceptions are for recoverable, external conditions. Unchecked exceptions are for programming errors.

Keep this hierarchy in your head and every other rule falls out naturally.

ExceptionHierarchyDemo.java · JAVA
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748
public class ExceptionHierarchyDemo {

    public static void main(String[] args) {

        // --- Checked Exception Example ---
        // IOException extends Exception directly, so the compiler
        // FORCES us to handle or declare it. Removing the try-catch
        // causes a compile error: "unreported exception IOException".
        try {
            readConfigFile("/etc/app/config.properties");
        } catch (java.io.IOException ioException) {
            // We handle the realistic possibility that the file isn't there
            System.out.println("Config file problem: " + ioException.getMessage());
        }

        // --- Unchecked Exception Example ---
        // NumberFormatException extends RuntimeException, so no compile
        // error if we skip the try-catch. The compiler trusts us.
        // But at runtime, passing a bad string will blow up here.
        String rawUserInput = "42"; // pretend this came from a form field
        int parsedAge = Integer.parseInt(rawUserInput); // safe with "42"
        System.out.println("Parsed age: " + parsedAge);

        // Now intentionally cause an unchecked exception to show the output
        String corruptInput = "forty-two";
        try {
            int badParse = Integer.parseInt(corruptInput);
        } catch (NumberFormatException numberFormatException) {
            // NumberFormatException is unchecked — we only catch it
            // at the boundary where raw input enters our system
            System.out.println("Bad input caught at boundary: " + numberFormatException.getMessage());
        }

        // Demonstrate the hierarchy programmatically
        NumberFormatException nfe = new NumberFormatException("demo");
        System.out.println("Is RuntimeException? " + (nfe instanceof RuntimeException)); // true
        System.out.println("Is Exception?        " + (nfe instanceof Exception));        // true

        java.io.IOException ioe = new java.io.IOException("demo");
        System.out.println("IOException is RuntimeException? " + (ioe instanceof RuntimeException)); // false
        System.out.println("IOException is Exception?        " + (ioe instanceof Exception));        // true
    }

    // 'throws IOException' is mandatory — omitting it is a compile error
    static void readConfigFile(String filePath) throws java.io.IOException {
        java.nio.file.Files.readAllBytes(java.nio.file.Paths.get(filePath));
    }
}
▶ Output
Config file problem: /etc/app/config.properties (No such file or directory)
Parsed age: 42
Bad input caught at boundary: For input string: "forty-two"
Is RuntimeException? true
Is Exception? true
iIOException is RuntimeException? false
IOException is Exception? true
🔥
The One-Line Rule:Extends Exception directly → checked (compiler enforces handling). Extends RuntimeException → unchecked (compiler stays silent). That's the entire distinction. Everything else is a consequence of this.

Writing Custom Exceptions That Actually Communicate Intent

Throwing Exception or RuntimeException directly is the exception equivalent of logging 'something went wrong'. Custom exceptions are how you make failures self-documenting.

The decision of which to extend is a design contract. When you extend Exception, you're telling every caller: 'this failure mode is realistic and environmental — you need to have a plan.' A PaymentGatewayException should be checked because a payment gateway being unreachable is a real-world condition your caller must handle gracefully.

When you extend RuntimeException, you're saying: 'this is a programming contract violation — if you use my API correctly, this never fires.' An InvalidOrderStateException for a state machine, where transitioning from SHIPPED back to PENDING is logically impossible, belongs as unchecked. It means the calling code has a bug.

A practical pattern: create a checked base exception for your domain (e.g., InventoryException) and let specific subtypes inherit it. This lets callers catch broadly when they need to, or narrowly when they can recover from specific cases. Always provide a constructor that accepts a cause parameter — wrapping lower-level exceptions preserves the stack trace and is critical for debugging production issues.

CustomExceptionDesign.java · JAVA
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113
// ── Checked: realistic external failure the caller must plan for ──
class PaymentGatewayException extends Exception {

    private final int httpStatusCode;

    public PaymentGatewayException(String message, int httpStatusCode) {
        super(message);
        this.httpStatusCode = httpStatusCode;
    }

    // Always include a cause constructor — wrapping preserves stack traces
    public PaymentGatewayException(String message, int httpStatusCode, Throwable cause) {
        super(message, cause); // passes the original exception up the chain
        this.httpStatusCode = httpStatusCode;
    }

    public int getHttpStatusCode() {
        return httpStatusCode;
    }
}

// ── Unchecked: programming contract violation, not environmental failure ──
class InvalidOrderStateException extends RuntimeException {

    private final String fromState;
    private final String toState;

    public InvalidOrderStateException(String fromState, String toState) {
        super(String.format(
            "Illegal state transition: cannot move order from [%s] to [%s]",
            fromState, toState
        ));
        this.fromState = fromState;
        this.toState   = toState;
    }

    public String getFromState() { return fromState; }
    public String getToState()   { return toState;   }
}

// ── Service that uses both ──
class OrderService {

    enum OrderStatus { PENDING, CONFIRMED, SHIPPED, DELIVERED }

    // 'throws PaymentGatewayException' is REQUIRED by the compiler
    // because PaymentGatewayException is checked. This is the contract
    // telling every caller: handle the gateway being unavailable.
    public void chargeCustomer(String customerId, double amountInDollars)
            throws PaymentGatewayException {

        boolean gatewayReachable = simulateGatewayCall();

        if (!gatewayReachable) {
            // Wrap the low-level HTTP detail with a meaningful domain exception
            throw new PaymentGatewayException(
                "Payment gateway timed out for customer: " + customerId,
                504
            );
        }
        System.out.println("Charged $" + amountInDollars + " to customer " + customerId);
    }

    // No 'throws' declaration needed — unchecked exceptions are silent contracts
    public void transitionOrderStatus(OrderStatus current, OrderStatus next) {

        // SHIPPED → PENDING is a programmer error, not an environmental condition
        if (current == OrderStatus.SHIPPED && next == OrderStatus.PENDING) {
            throw new InvalidOrderStateException(current.name(), next.name());
        }
        System.out.println("Order moved: " + current + " → " + next);
    }

    private boolean simulateGatewayCall() {
        return false; // simulating an unreachable gateway
    }
}

public class CustomExceptionDesign {

    public static void main(String[] args) {

        OrderService orderService = new OrderService();

        // Checked exception — compiler REQUIRES this try-catch
        try {
            orderService.chargeCustomer("CUST-9912", 149.99);
        } catch (PaymentGatewayException paymentException) {
            // Caller handles the recoverable failure: maybe retry, or alert the user
            System.out.println("Payment failed (HTTP " + paymentException.getHttpStatusCode()
                + "): " + paymentException.getMessage());
            System.out.println("Queuing payment for retry...");
        }

        // Unchecked exception — no try-catch required by compiler
        // Only catch it at your application's outermost error boundary
        orderService.transitionOrderStatus(
            OrderService.OrderStatus.CONFIRMED,
            OrderService.OrderStatus.SHIPPED
        );

        // This next line would throw InvalidOrderStateException at runtime
        // because SHIPPED → PENDING is a logic bug in the calling code
        try {
            orderService.transitionOrderStatus(
                OrderService.OrderStatus.SHIPPED,
                OrderService.OrderStatus.PENDING
            );
        } catch (InvalidOrderStateException stateException) {
            System.out.println("Bug caught: " + stateException.getMessage());
        }
    }
}
▶ Output
Payment failed (HTTP 504): Payment gateway timed out for customer: CUST-9912
Queuing payment for retry...
Order moved: CONFIRMED → SHIPPED
Bug caught: Illegal state transition: cannot move order from [SHIPPED] to [PENDING]
⚠️
Pro Tip:Always add a constructor that accepts a `Throwable cause` to your custom exceptions — even if you don't use it today. Wrapping a low-level `SQLException` inside your `RepositoryException` without the cause discards the original stack trace, making production debugging nearly impossible.

The Real-World Pattern: Where Each Exception Type Belongs

Knowing the definition is one thing. Knowing where to put each type in a layered application is what separates a junior from a mid-level engineer.

In a typical web application you have an infrastructure layer (database, HTTP clients, file I/O), a service layer (business logic), and a presentation layer (controllers, API endpoints). Checked exceptions are native to the infrastructure layer — SQLException, IOException, SSLException. These are environmental realities.

Here's the key pattern: you should almost always catch the checked infrastructure exception at the boundary between infrastructure and service layers and wrap it in an unchecked domain exception before rethrowing. Why? Because your service layer shouldn't be coupled to java.sql.SQLException. It should speak domain language. And if you force every service method to throws SQLException, that implementation detail leaks all the way up to your controller.

Modern frameworks like Spring lean heavily on this — Spring Data wraps SQLExceptions into unchecked DataAccessExceptions precisely so your business logic stays clean. This wrapping pattern also means the original cause is preserved for your logs, while callers aren't burdened with handling infrastructure concerns they can't meaningfully recover from anyway.

Use checked exceptions when your direct caller can realistically take a different action based on the failure. Use unchecked when the failure means 'the code calling me has a bug' or 'no caller can meaningfully recover from this.'

LayeredExceptionPattern.java · JAVA
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
import java.sql.*;

// ── Domain-level unchecked exception — service layer speaks in domain terms ──
class UserRepositoryException extends RuntimeException {
    public UserRepositoryException(String message, Throwable cause) {
        super(message, cause); // ALWAYS preserve the cause for stack trace logging
    }
}

class UserNotFoundException extends RuntimeException {
    private final long userId;
    public UserNotFoundException(long userId) {
        super("No user found with ID: " + userId);
        this.userId = userId;
    }
    public long getUserId() { return userId; }
}

// ── Infrastructure layer: wraps checked JDBC exceptions into unchecked domain ones ──
class UserRepository {

    private final Connection databaseConnection;

    public UserRepository(Connection databaseConnection) {
        this.databaseConnection = databaseConnection;
    }

    public String findUsernameById(long userId) {
        String query = "SELECT username FROM users WHERE id = ?";

        try {
            PreparedStatement statement = databaseConnection.prepareStatement(query);
            statement.setLong(1, userId);
            ResultSet results = statement.executeQuery();

            if (!results.next()) {
                // Business rule violation — unchecked, the caller should validate input
                throw new UserNotFoundException(userId);
            }

            return results.getString("username");

        } catch (SQLException sqlException) {
            // KEY PATTERN: catch the infrastructure checked exception here,
            // wrap it in our domain unchecked exception, preserve the cause.
            // The service layer never sees SQLException — it stays decoupled.
            throw new UserRepositoryException(
                "Database error while fetching user ID: " + userId,
                sqlException // <-- cause preserved for logging
            );
        }
    }
}

// ── Service layer: clean business logic, no SQL concerns ──
class UserProfileService {

    private final UserRepository userRepository;

    public UserProfileService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    // Notice: no 'throws' clause. The service layer is clean.
    // If UserRepositoryException propagates, the controller's global
    // error handler catches it and returns a 500. Clean separation.
    public String buildWelcomeMessage(long userId) {
        String username = userRepository.findUsernameById(userId); // no try-catch needed
        return "Welcome back, " + username + "!";
    }
}

// ── Presentation layer: only catches what it can meaningfully respond to ──
class UserController {

    private final UserProfileService profileService;

    public UserController(UserProfileService profileService) {
        this.profileService = profileService;
    }

    public String handleGetProfile(long userId) {
        try {
            return profileService.buildWelcomeMessage(userId);
        } catch (UserNotFoundException notFoundException) {
            // Caught specifically — return a 404 response
            return "404: User " + notFoundException.getUserId() + " not found";
        }
        // UserRepositoryException (database failure) is NOT caught here.
        // It propagates to the framework's global exception handler → 500 response.
        // That's the correct behaviour: the controller can't fix a database outage.
    }
}

public class LayeredExceptionPattern {

    public static void main(String[] args) throws Exception {
        // Simulate the lookup of a non-existent user (no real DB needed)
        // We'll mock the behaviour directly to show the flow

        // Simulating UserNotFoundException path
        UserController controller = buildMockController(false);
        String response = controller.handleGetProfile(99L);
        System.out.println("Controller response: " + response);
    }

    // Creates a controller backed by a fake repository for demo purposes
    static UserController buildMockController(boolean userExists) {
        UserRepository mockRepo = new UserRepository(null) {
            @Override
            public String findUsernameById(long userId) {
                if (!userExists) {
                    throw new UserNotFoundException(userId);
                }
                return "alice";
            }
        };
        return new UserController(new UserProfileService(mockRepo));
    }
}
▶ Output
Controller response: 404: User 99 not found
⚠️
Watch Out:Never let `SQLException` or `IOException` leak through your service layer interface. The moment your `UserService.findById()` signature says `throws SQLException`, your business logic is coupled to your database driver — a change of DB technology means rewriting every caller. Wrap and rethrow as unchecked domain exceptions at the repository boundary.

Common Mistakes That Trip Up Intermediate Developers

Even developers who understand the theory make these mistakes under pressure. Here are the three that cause the most damage in real codebases.

Swallowing exceptions is the silent killer. An empty catch block turns a detectable failure into a ghost — the system behaves wrongly with no evidence of why. If you genuinely can't handle an exception, log it and rethrow, or convert it to an unchecked exception. Never leave a catch block empty in production code.

Exception pollution is the checked-exception version of the problem. When a method deep in the stack throws a checked exception, inexperienced developers propagate it up every method signature rather than wrapping it. You end up with controllers declaring throws SQLException — a leaky abstraction that defeats the whole layered architecture.

Catching Exception or Throwable too broadly masks completely different failure modes under one handler. Catching Exception to log-and-continue will silently swallow NullPointerException from your own bugs alongside the IOException you intended to catch. Catch the most specific type you can act on.

ExceptionMistakesAndFixes.java · JAVA
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
import java.io.*;
import java.util.logging.*;

public class ExceptionMistakesAndFixes {

    private static final Logger logger =
        Logger.getLogger(ExceptionMistakesAndFixes.class.getName());

    // ════════════════════════════════════
    // MISTAKE 1: Swallowing the exception
    // ════════════════════════════════════

    static void badReadFile(String filePath) {
        try {
            new FileInputStream(filePath);
        } catch (FileNotFoundException e) {
            // ❌ WRONG: empty catch block. The failure disappears.
            // The caller gets null behaviour with zero explanation.
        }
    }

    static void goodReadFile(String filePath) throws FileNotFoundException {
        try {
            new FileInputStream(filePath);
        } catch (FileNotFoundException fileNotFoundException) {
            // ✅ RIGHT: log it, then either handle it or rethrow
            logger.severe("Cannot open config file at: " + filePath);
            throw fileNotFoundException; // rethrow so the caller knows
        }
    }

    // ═══════════════════════════════════════════════
    // MISTAKE 2: Catching Exception too broadly
    // ═══════════════════════════════════════════════

    static void badBroadCatch(String[] items, int index, String filePath) {
        try {
            String item   = items[index];          // might throw ArrayIndexOutOfBoundsException
            new FileInputStream(filePath);         // might throw FileNotFoundException
            Integer.parseInt(item);                // might throw NumberFormatException
        } catch (Exception e) {
            // ❌ WRONG: all three failures look identical here.
            // A bug in your index logic is hidden alongside a missing file.
            System.out.println("Something failed: " + e.getMessage());
        }
    }

    static void goodSpecificCatch(String[] items, int index, String filePath) {
        // ✅ RIGHT: handle each failure mode where you can act on it specifically
        if (index < 0 || index >= items.length) {
            // Validate input before use — don't rely on catching your own bug
            throw new IllegalArgumentException(
                "Index " + index + " is out of range for array of size " + items.length
            );
        }

        String item = items[index];

        try {
            new FileInputStream(filePath);
        } catch (FileNotFoundException fileNotFoundException) {
            logger.warning("File not found: " + filePath);
            // Handle the file-missing case specifically — maybe use a default
        }

        try {
            Integer.parseInt(item);
        } catch (NumberFormatException numberFormatException) {
            logger.warning("Item '" + item + "' is not a valid integer");
            // Handle the bad-format case specifically
        }
    }

    // ═══════════════════════════════════════════════════════
    // MISTAKE 3: Losing the original cause when wrapping
    // ═══════════════════════════════════════════════════════

    static class DataLoadException extends RuntimeException {
        public DataLoadException(String message, Throwable cause) {
            super(message, cause);
        }
    }

    static void badWrap(String filePath) {
        try {
            new FileInputStream(filePath);
        } catch (FileNotFoundException fileNotFoundException) {
            // ❌ WRONG: the original FileNotFoundException and its stack
            // trace are thrown away. In production logs, you'll only see
            // DataLoadException with no trace back to the root cause.
            throw new DataLoadException("Could not load data", null);
        }
    }

    static void goodWrap(String filePath) {
        try {
            new FileInputStream(filePath);
        } catch (FileNotFoundException fileNotFoundException) {
            // ✅ RIGHT: pass the original exception as the cause.
            // Both exceptions appear in the stack trace.
            throw new DataLoadException(
                "Could not load data from: " + filePath,
                fileNotFoundException // <-- cause preserved
            );
        }
    }

    public static void main(String[] args) {
        // Demonstrate the cause-preservation difference
        try {
            goodWrap("/no/such/file.csv");
        } catch (DataLoadException dataLoadException) {
            System.out.println("Top-level message: " + dataLoadException.getMessage());
            System.out.println("Root cause:        " + dataLoadException.getCause().getMessage());
        }
    }
}
▶ Output
Top-level message: Could not load data from: /no/such/file.csv
Root cause: /no/such/file.csv (No such file or directory)
⚠️
The Golden Rule:If you catch it and can't fully handle it, you must either log-and-rethrow, or wrap-and-rethrow with the original as the cause. A catch block that just prints a message and lets execution continue is almost always a bug waiting to surface in the worst possible moment.
AspectChecked ExceptionUnchecked Exception
ExtendsException (directly)RuntimeException
Compiler enforcementMust handle or declare with 'throws'No compiler requirement
Typical causeExternal/environmental failure (file, network, DB)Programming error or contract violation
Real-world examplesIOException, SQLException, ParseExceptionNullPointerException, IllegalArgumentException, ArrayIndexOutOfBoundsException
Caller expectationCaller can meaningfully recoverCaller should fix the code, not catch
Method signature impactAppears in 'throws' clause — part of the public APIInvisible in signature — implicit contract
Where to catchWherever you can take meaningful recovery actionAt system boundaries (global error handlers)
Custom exception patternExtend Exception; use for recoverable domain failuresExtend RuntimeException; use for API contract violations
Spring framework approachRare — Spring wraps most into uncheckedPreferred — DataAccessException hierarchy is all unchecked

🎯 Key Takeaways

  • The entire distinction is in the inheritance tree: extends Exception directly = checked (compiler enforces it); extends RuntimeException = unchecked (compiler ignores it). No other magic.
  • Checked exceptions are a compiler-enforced contract saying 'this realistic external failure will happen — you must have a plan.' Unchecked exceptions say 'your code has a bug — fix it, don't catch it.'
  • Wrap checked infrastructure exceptions (SQLException, IOException) into unchecked domain exceptions at your layer boundaries — this keeps business logic clean and prevents implementation details from leaking through your API signatures.
  • Always pass the original exception as the 'cause' when wrapping — losing the cause makes production debugging brutally difficult because the root stack trace is discarded forever.

⚠ Common Mistakes to Avoid

  • Mistake 1: Swallowing exceptions with empty catch blocks — The program appears to run but produces wrong results silently, with no log entry and no stack trace to debug from — Add at minimum a logger.error() call and either rethrow the exception or wrap it in an unchecked exception before rethrowing.
  • Mistake 2: Propagating checked exceptions across architectural boundaries — Your service method ends up with 'throws SQLException', coupling your business logic to your database driver and leaking infrastructure details through every layer — Catch the checked infrastructure exception at the repository/DAO layer, wrap it in an unchecked domain exception (passing the original as the cause), and let that propagate instead.
  • Mistake 3: Losing the original cause when wrapping exceptions — You throw 'new MyException(message, null)' instead of passing the caught exception as the cause — your production logs show the wrapper message but the original stack trace pointing to the real failure line is gone forever — Always pass the caught exception as the second argument: 'new MyException(message, originalException)'.

Interview Questions on This Topic

  • QCan you explain the difference between checked and unchecked exceptions in Java, and give a design reason why both exist rather than just having one type?
  • QIf you're building a repository layer that calls JDBC and throws SQLException, would you expose that checked exception in your service layer interface? Why or why not — and how would you handle it?
  • QRuntimeException is a subclass of Exception, so why doesn't catching Exception also force you to catch RuntimeException — and does catching Exception actually catch unchecked exceptions at runtime?

Frequently Asked Questions

Can you convert a checked exception to an unchecked exception in Java?

Yes — and this is a common, recommended pattern. Catch the checked exception and rethrow it wrapped inside a class that extends RuntimeException, passing the original as the cause constructor argument. This is exactly what Spring does with SQLException → DataAccessException. The key is to always pass the original exception as the cause so the full stack trace is preserved in your logs.

Should I use checked or unchecked exceptions for my custom business exceptions?

Use checked exceptions when the failure is environmental and the direct caller can take a meaningful alternative action — for example, a payment gateway being unreachable. Use unchecked exceptions when the failure represents a programming contract violation — for example, passing a null order ID to a method that explicitly requires one. When in doubt, modern Java practice (and frameworks like Spring) leans toward unchecked to avoid polluting method signatures.

Does catching Exception catch both checked and unchecked exceptions?

Yes — at runtime, catching Exception catches everything except Errors, because RuntimeException is a subclass of Exception. This is precisely why catching Exception broadly is dangerous: you'll accidentally swallow NullPointerExceptions and other programming bugs alongside the specific checked exception you intended to handle. Always catch the most specific exception type you can meaningfully act on.

🔥
TheCodeForge Editorial Team Verified Author

Written and reviewed by senior developers with real-world experience across enterprise, startup and open-source projects. Every article on TheCodeForge is written to be clear, accurate and genuinely useful — not just SEO filler.

← Previousthrows and throw in JavaNext →Multi-catch and Finally Block
Forged with 🔥 at TheCodeForge.io — Where Developers Are Forged