Skip to content
ExponaBLOG
JAVA, SPRING FRAMEWORK & SPRING BOOT · DAY 010 / 100

Checked vs unchecked exceptions in Java, explained simply

When to force callers to handle errors, and when to let them bubble.

Expona Admin7 min read

Introduction

One of the most misunderstood aspects of error handling in Java is the difference between checked and unchecked exceptions. Most developers learn to throw and catch exceptions early in their journey, but the question of when to force callers to handle them rarely gets the attention it deserves. Understanding checked vs unchecked exceptions in Java, explained simply, comes down to one principle: should the caller be forced to make a decision about what to do when something goes wrong?

This distinction shapes how you design APIs, how Spring Framework handles errors, and ultimately how maintainable your codebase becomes. Get this wrong, and you'll end up with layers of try-catch blocks that hide real problems. Get it right, and your error handling becomes a natural part of your code's flow.

Checked exceptions: recoverable conditions the caller must decide on

A checked exception is one that extends Exception but not RuntimeException. The Java compiler forces you to either catch it or declare it in your method signature using throws. The philosophy here is straightforward: if something might go wrong in a way that the caller can reasonably recover from, make that explicit in the contract.

Consider a real scenario from an e-commerce system. When you try to read a customer's order history from a database file:

java
public class OrderFileReader {
    public List<Order> readOrdersFromDisk(String filePath) throws IOException {
        FileReader reader = new FileReader(filePath);
        // ... parse and return orders
        return orders;
    }
}

The IOException is checked. The caller sees the throws declaration and knows: "This method might not find the file, the disk might be full, or permissions might be denied." The caller can then make an informed decision. Maybe they retry with a different path. Maybe they return a cached version. Maybe they fail gracefully to the user.

This is the intent of checked exceptions: they represent expected failure modes that a reasonable program might recover from. They're saying "this is a known risk, and you should think about how to handle it."

However, checked exceptions have become controversial in modern Java. The overhead of declaring them everywhere, combined with the temptation to catch them and rethrow or ignore them, often defeats their purpose. Spring Framework actually moves away from this pattern, which we'll explore later.

Unchecked exceptions: programming errors and unrecoverable failures

Unchecked exceptions extend RuntimeException. The compiler doesn't force you to catch or declare them, which means they're not part of your method's contract. They signal something that shouldn't happen in normal operation—a bug, an invalid state, or a catastrophic failure that the caller can't reasonably recover from.

Think about a trading system processing market data. If you try to access an order by ID and it doesn't exist:

java
public class TradeService {
    public Trade getTrade(String tradeId) {
        Trade trade = tradeRepository.findById(tradeId);
        if (trade == null) {
            throw new IllegalArgumentException("Trade not found: " + tradeId);
        }
        return trade;
    }
}

This is an unchecked exception because it signals a bug in the caller's logic—they asked for a trade that doesn't exist. The caller can't "recover" from this in a meaningful way. They should fix their code. It's not a transient network hiccup or a file that moved; it's a logical error.

Another classic unchecked scenario: a NullPointerException. You tried to call a method on a null reference. That's a programming error, full stop. The framework can't help you recover; you need to fix the code.

Spring's data access layer leans heavily on unchecked exceptions. When you try to insert a duplicate key, Spring wraps the database's checked SQLException in an unchecked DataIntegrityViolationException. The reasoning: your ORM layer handles the technical exception; your business layer shouldn't need to know that it came from the database.

Most modern APIs and Spring prefer unchecked exceptions

If you've used modern Java libraries—from Spring to Google's Guava to the JDK itself—you've noticed a strong bias toward unchecked exceptions. This isn't accidental. The Java community has collectively concluded that checked exceptions, while well-intentioned, created more problems than they solved.

Here's why Spring Framework prefers unchecked exceptions:

  • Reduced boilerplate: You're not constantly wrapping exceptions in try-catch blocks that do nothing but rethrow.
  • Cleaner code: Business logic remains readable without layers of exception handling.
  • Natural propagation: If something goes wrong, let the exception bubble to where it can actually be handled (often a global error handler or middleware).
  • Real recovery patterns: Most recovery doesn't happen at the point of the throw; it happens in a central handler that can log, alert, and respond.

In a ride-hailing system, when you query the database for available drivers, you might write:

java
public class DriverService {
    public List<Driver> findAvailableDrivers(Location location) {
        return driverRepository.findByLocationAndStatus(location, "AVAILABLE");
    }
}

The repository layer handles database exceptions (checked SQLException) and converts them to unchecked exceptions like DataAccessException. Your service layer doesn't need to know or care about SQL errors. If the database is down, the global exception handler catches it and returns a 503 Service Unavailable. Clean, simple, and maintainable.

When to use each type in practice

So when do you actually use checked exceptions in modern codebases? The answer: rarely, and only when you truly expect callers to recover in different ways.

Consider a payment processing API in an SaaS platform:

java
public class PaymentGateway {
    // Checked exception: caller might want to retry or fallback
    public PaymentResult charge(String customerId, BigDecimal amount) 
            throws PaymentServiceUnavailableException {
        // Call external payment processor
    }
}

Here, the service might be temporarily down, and the caller could reasonably retry. It's an expected failure mode. But notice the specificity: not a generic Exception, but a descriptive checked exception that signals exactly what happened.

Contrast this with:

java
public PaymentResult charge(String customerId, BigDecimal amount) 
        throws IllegalArgumentException {
    if (customerId == null || customerId.isEmpty()) {
        throw new IllegalArgumentException("customerId must not be empty");
    }
    // ...
}

This is unchecked because it's a validation error, not a recoverable failure. The caller shouldn't try to catch and handle it; they should fix their input.

Never catch Exception just to log and rethrow

This anti-pattern appears in almost every codebase. Someone thinks, "I should log this error," and writes:

java
public void processOrder(Order order) {
    try {
        saveToDatabase(order);
    } catch (Exception e) {
        logger.error("Failed to save order", e);
        throw e;  // Why?
    }
}

This adds nothing. The exception will bubble up and get logged by your global error handler anyway. All you've done is add noise to the stack trace and made the method harder to read.

If you need to do something before rethrowing—like cleanup or adding context—do it:

java
public void processOrder(Order order) {
    try {
        saveToDatabase(order);
    } catch (DataIntegrityViolationException e) {
        // Actually handle: maybe update cache, maybe call fallback service
        fallbackNotificationService.notifyFailed(order);
        throw new OrderProcessingException("Could not persist order after 3 retries", e);
    }
}

This catches a specific unchecked exception, takes meaningful action, and rethrows with additional context. That's legitimate. But catching Exception generically and doing nothing is cargo cult programming.

Spring's transactional boundaries actually handle this for you. If an unchecked exception bubbles out of a method marked @Transactional, Spring rolls back. You don't need to catch and rethrow for that behavior.

Common mistakes

Declaring throws Exception: Never do this. It's like saying "something might go wrong, but I don't know what." Be specific. If you're delegating upward, only declare the exceptions that actually escape your method.

Catching broadly to convert checked to unchecked: This is legitimate if done intentionally. But catching, logging nothing, and rethrowing is wasteful:

java
// Bad
try {
    reader.readFile();
} catch (IOException e) {
    throw new RuntimeException(e);  // Lost the original exception type
}

// Better
try {
    reader.readFile();
} catch (IOException e) {
    throw new UncheckedIOException(e);  // Preserves the cause
}

Over-designing recovery logic: Most applications don't need elaborate per-method error handling. A global error handler, combined with strategic try-catch at integration boundaries (REST controllers, message consumers), handles 90% of cases cleanly.

Mixing checked and unchecked in the same layer: Be consistent. If your service layer throws unchecked exceptions, don't suddenly throw a checked exception from one method. It breaks the caller's expectations and encourages bad handling.

Key takeaways

  • Checked exceptions force callers to decide how to handle a failure; use them only for truly recoverable, expected failure modes.
  • Unchecked exceptions signal bugs or unrecoverable failures; they're the default in modern Java and Spring.
  • Spring converts checked exceptions (like SQLException) to unchecked exceptions (like DataAccessException) so your business logic stays clean.
  • Never catch exceptions just to log and rethrow; let them bubble to a global handler.
  • Be specific with exception types; declaring throws Exception or catching broadly hides real problems.
  • Focus on recovery at integration boundaries (REST controllers, message handlers, scheduled tasks), not in every method.
  • If you're debating whether to use checked or unchecked, unchecked is almost always the right choice in modern codebases.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles