Day 07 · Week 1 · Foundations

Exceptions, done right.

Exception handling is where most learners pile up bad habits. Today we settle the checked-vs-unchecked argument once, fix the four mistakes everyone makes, and preview how Spring's @ControllerAdvice replaces try-catch chains in real APIs.

~22 min readdesign rulesSpring preview
You can't ship a Java backend without exception handling. Worse — you can't pass an interview without explaining the checked-vs-unchecked debate confidently. Today we settle both.

Why exceptions exist — the 30-second version

Before exceptions (in C, for instance), errors were communicated by return codes. Every function call had to be wrapped:

// Pseudo-C — the dark ages
int result = openFile(path);
if (result == ERR_NOT_FOUND) { /* handle */ }
if (result == ERR_PERMISSION) { /* handle */ }
// for every single function call... forever

Three big problems:

  1. If you forget to check, the error silently propagates as if it were a normal value
  2. The "happy path" is buried under error checks
  3. Errors cannot carry context (no stack trace, no message)

Exceptions fix all three. They separate the happy path from the error path, propagate up automatically until handled, and carry rich context (message + stack trace + cause).

The exception hierarchy you must know

ThrowableErrorJVM-level (OutOfMemoryError)don't catch theseExceptionRuntimeExceptionUNCHECKEDNullPointer, IllegalArgument...Other ExceptionsCHECKEDIOException, SQLException...
Throwable is the root. Most of your code will throw RuntimeException or its subclasses.

Three categories you need to know

The Great Debate · checked vs. unchecked

This is one of the most polarizing topics in Java. Here's the honest take:

Checked exceptions — the original idea

  • Goal: force callers to handle predictable failure modes
  • Compiler checks: throws declarations on signatures
  • Examples: IOException, SQLException
  • In practice: developers wrap them in try/catch with empty bodies just to silence the compiler — which is worse than no exception at all

Unchecked — the modern preference

  • Don't pollute every method signature with throws
  • Catch where you can actually do something
  • Spring, Hibernate, and most modern frameworks throw unchecked exceptions
  • You can wrap a checked exception in a RuntimeException when needed
📌 The honest interview answer

Checked exceptions sounded right in theory but failed in practice. Modern Java code (and Spring entirely) prefers unchecked exceptions. Use checked only when the caller has a realistic recovery for the failure (rare). Otherwise — unchecked.

This is also why Spring wraps SQLException (checked) into DataAccessException (unchecked). One of Spring's earliest design wins.

The mechanics — try / catch / finally

try {
  riskyOperation();
} catch (FileNotFoundException e) {
  // specific catch first
  logger.warn("file missing: {}", e.getMessage());
} catch (IOException e) {
  // broader catch second
  logger.error("IO failed", e);
} finally {
  // always runs — clean up resources
  closeStuff();
}

try-with-resources (Java 7+) — the modern way

Anything implementing AutoCloseable can be opened in a special try(...) block. The resource is auto-closed at the end, even on exception:

try (BufferedReader reader = new BufferedReader(new FileReader(path))) {
  return reader.readLine();
} // reader.close() called automatically — no finally needed

You will write this a lot when handling JDBC connections, file streams, HTTP clients.

Custom exceptions — for your domain

Generic RuntimeException is bad practice. Domain-specific exceptions make logs and tests dramatically clearer:

public class UserNotFoundException extends RuntimeException {
  public UserNotFoundException(Long id) {
    super("User not found: " + id);
  }
}

// Usage
User user = userRepo.findById(id)
    .orElseThrow(() -> new UserNotFoundException(id));
🧠 The connection to Spring

In a Spring REST controller, you'll throw UserNotFoundException from your service. A class annotated @ControllerAdvice centrally catches it and translates it to a 404 Not Found response. Your service doesn't know about HTTP. The advice doesn't know about the database. Clean separation. We'll build this properly in Week 4 (Day 28).

The four mistakes everyone makes

❌ Don't do this

// 1. Empty catch block
try { ... }
catch (Exception e) { } // 💀

// 2. catch (Exception)
catch (Exception e) { ... }

// 3. Lose the original
catch (IOException e) {
  throw new RuntimeException(
    "oops"); // no cause!
}

// 4. Use exceptions for flow control
try { return map.get(k).foo(); }
catch (NPE e) { return null; }

✅ Do this instead

// 1. Always log or rethrow
catch (Exception e) {
  log.error("failed", e);
  throw e;
}

// 2. Catch specific types
catch (IOException e) { ... }

// 3. Wrap with cause
catch (IOException e) {
  throw new ServiceException(
    "loading failed", e);
}

// 4. Just check first
return map.containsKey(k)
  ? map.get(k).foo() : null;

Exception chaining — preserve the cause

When you wrap a low-level exception in a domain one, pass the original as the cause:

try { fileService.load(path); }
catch (IOException e) {
  throw new ConfigLoadException("could not load: " + path, e); // pass e as cause
}

This way the stack trace will show both exceptions chained — your domain one and the underlying IOException. Crucial for debugging in production.

Preview · how Spring handles exceptions

Forget every try/catch in your controllers. Spring lets you centralize all exception → HTTP-response mapping in one class:

@RestControllerAdvice
public class GlobalExceptionHandler {

  @ExceptionHandler(UserNotFoundException.class)
  public ResponseEntity<ErrorBody> handleNotFound(UserNotFoundException e) {
    return ResponseEntity.status(404)
        .body(new ErrorBody("USER_NOT_FOUND", e.getMessage()));
  }

  @ExceptionHandler(MethodArgumentNotValidException.class)
  public ResponseEntity<ErrorBody> handleValidation(...) { ... }
}

Throw your domain exception in a service. Spring's advice catches it, returns the correct HTTP status. Your controllers stay clean. This is the modern pattern. We'll build this in detail in Week 4.

One subtle gotcha — finally + return

If you return in try and also in finally, the finally wins. This causes nasty bugs:

int weird() {
  try { return 1; }
  finally { return 2; } // 💥 weird() returns 2, not 1
}

Rule: never return from a finally block. Use it only for cleanup.

The Day-7 cheat sheet

QuestionAnswer
Should I use checked or unchecked?Unchecked. Period. (Unless you have a real reason)
Should I catch Exception?Almost never. Catch the specific subclass.
What about Errors?Don't catch.
Where do I close my JDBC connection?Try-with-resources. Never finally manually.
How should I name custom exceptions?Domain-specific: UserNotFoundException, InsufficientFundsException.
Should controllers have try-catch?No. Use @ControllerAdvice centrally.
🎉 You finished Week 1

You now have:

Week 2 takes us into the web era — HTTP deep dive, SQL essentials, raw JDBC pain, Servlets walkthrough, and REST architecture. By the end of Week 2 you're ready to start with Spring.

Pause & reflect

Lock in today's learning

Last reflection of Week 1. Take it seriously.

  1. What's the difference between checked and unchecked exceptions, and which does modern Java prefer?
  2. Why is catching Exception almost always wrong?
  3. What does try-with-resources do, and what interface must a class implement to be usable in it?
  4. Why is exception chaining (passing the cause) crucial in production?
  5. Sketch (in words) how @ControllerAdvice turns a thrown exception into an HTTP response.
  6. What four mistakes did we identify, and what's the right pattern for each?

End of Day 7 — and Week 1. Take a day off. Then we begin Week 2.