This chapter moves the project from code that mostly works to code that fails clearly and safely. You will learn exception fundamentals, checked versus unchecked exceptions, try-catch-finally, custom exceptions, fail-fast validation, and error design through checkout and order-processing examples while keeping the real focus on core Java reliability theory.

Why Chapter 6 is about reliability, not just syntax

By Chapter 5, the project has richer models and cleaner abstractions. But well-shaped code can still fail badly if it does not handle invalid state and failure paths clearly. A checkout flow can receive bad input, missing data, impossible state transitions, or downstream failures. Java exceptions are the language tool for representing that reality.

The important shift in this chapter is mental, not only syntactic. Exceptions are not just something to catch because the compiler complains. They are part of API design, part of program correctness, and part of how a reader understands what the code expects and what it refuses to allow.

  • Exceptions make failure paths explicit.
  • Reliable code handles invalid state intentionally instead of ignoring it.
  • Interviewers often test whether you understand why an exception exists, not just where to type catch.

if (quantity <= 0) {
    throw new IllegalArgumentException("Quantity must be positive");
}

Checked and unchecked exceptions

Java separates exceptions into checked and unchecked categories. Checked exceptions represent problems the caller is often expected to consider and handle, while unchecked exceptions usually represent programming mistakes, invalid usage, or broken assumptions. The design question is not which category is more advanced. The real question is which category communicates the API contract honestly.

A strong answer explains recoverability and responsibility. If the caller can reasonably do something useful, a checked exception may make sense. If the problem signals an invalid method call or broken state, an unchecked exception is often clearer.

  • Checked exceptions push the caller to consider recovery.
  • Unchecked exceptions usually signal bad input, misuse, or programming errors.
  • The category should match the contract and responsibility of the code.

public OrderReceipt loadReceipt(String orderId) throws IOException {
    return mapper.readValue(storage.read(orderId), OrderReceipt.class);
}

throw, throws, and propagation

New Java learners often confuse throw and throws. throw creates and raises one exception object at a specific point. throws declares in the method signature that a method may let certain exceptions escape to its caller. This distinction matters because it shapes how responsibility flows through the call stack.

Propagation is not always bad. Sometimes the current method is not the right place to decide recovery. Good Java code either handles an exception where it can genuinely respond or propagates it with enough context for a higher layer to act wisely.

  • throw raises one exception at a specific point in code.
  • throws documents possible propagation to callers.
  • A method should only catch what it can handle meaningfully.

public void submitOrder(Order order) throws PaymentGatewayException {
    paymentGateway.charge(order.total());
}

try catch finally and cleanup reasoning

try-catch-finally is where many developers first learn structured failure handling. The key idea is not memorizing block order. It is understanding which code belongs in normal flow, which belongs in recovery, and which belongs in cleanup that must run whether the operation succeeds or fails.

finally is valuable when cleanup must happen no matter what, but it should not become a place for hidden business logic. The cleaner the separation between useful handling and guaranteed cleanup, the easier the failure path is to trust.

  • try contains the operation that may fail.
  • catch should either recover or translate failure meaningfully.
  • finally exists for guaranteed cleanup, not for surprise business behavior.

try {
    processPayment(order);
} catch (PaymentGatewayException error) {
    markPaymentPending(order);
} finally {
    auditLogger.recordAttempt(order.id());
}

Custom exceptions and fail-fast validation

Generic exceptions can communicate too little. A custom exception can make the failure more precise and easier to reason about. This is useful when the code has a business-meaningful failure state such as attempting checkout on an empty cart or trying to reserve more stock than exists.

Fail-fast validation is closely related. The sooner invalid state is rejected, the easier the system is to trust. This chapter uses checkout examples, but the underlying Java lesson is broad: validate assumptions early and use exception types that help the reader understand what rule was broken.

  • Custom exceptions can make domain failures more explicit.
  • Fail-fast validation keeps invalid state from spreading.
  • Precision in error design improves both debugging and API clarity.

public class EmptyCartException extends RuntimeException {
    public EmptyCartException(String message) {
        super(message);
    }
}

Cause chaining and code-reading discipline

When lower-level code fails, higher-level code sometimes needs to add context without destroying the original cause. Cause chaining is how Java preserves both. That matters because debugging is much harder when the first meaningful stack trace is replaced by a vague wrapper with no history.

Interview questions on exceptions often become code-reading exercises. The real challenge is tracing what happens, what escapes, what is swallowed, and what message the next reader will have available. Reliable systems depend as much on clear failure context as they do on clever recovery logic.

  • Wrap exceptions with context when that context helps the next layer.
  • Preserve the original cause instead of hiding it.
  • Exception code should be read as a control-flow design, not just as syntax.

catch (SQLException error) {
    throw new IllegalStateException("Failed to save order " + orderId, error);
}

Chapter takeaway

Reliable Java code does not hide failure. It models what can go wrong, rejects invalid state early, preserves useful context, and makes failure paths easier to reason about in both interviews and real systems.