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.
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... foreverThree big problems:
- If you forget to check, the error silently propagates as if it were a normal value
- The "happy path" is buried under error checks
- 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
Three categories you need to know
- Error — JVM-level disasters (
OutOfMemoryError,StackOverflowError). Do not catch these. Let the JVM die — it can't recover. - Checked Exception — extends
Exceptionbut notRuntimeException. Compiler forces you to handle or declare them. Examples:IOException,SQLException. - Unchecked Exception — extends
RuntimeException. Compiler does not force handling. Examples:NullPointerException,IllegalArgumentException,IllegalStateException.
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:
throwsdeclarations on signatures - Examples:
IOException,SQLException - In practice: developers wrap them in
try/catchwith 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
RuntimeExceptionwhen needed
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 neededYou 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));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
| Question | Answer |
|---|---|
| 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 now have:
- The historical map of Java backend (Day 1)
- How Java actually executes — JVM, classloader, reflection (Day 2)
- Sharper OOP decisions — abstract vs interface, composition (Day 3)
- Collections internals & decision criteria (Day 4)
- Generics & type-erasure literacy (Day 5)
- Reading-fluent Java 8 streams, lambdas, Optional (Day 6)
- Modern exception design (Day 7)
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.
Lock in today's learning
Last reflection of Week 1. Take it seriously.
- What's the difference between checked and unchecked exceptions, and which does modern Java prefer?
- Why is catching
Exceptionalmost always wrong? - What does try-with-resources do, and what interface must a class implement to be usable in it?
- Why is exception chaining (passing the cause) crucial in production?
- Sketch (in words) how
@ControllerAdviceturns a thrown exception into an HTTP response. - 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.