Week 4 Day 2 — throw, throws, and catch
Goal
Today I want the mechanics of raising and handling an exception, including the cause chain.
Main questions:
- What does
throwdo to the stack? - What does
throwsdeclare? - How does
catchchoose a block? - How do I wrap with a cause?
- When is catching the wrong move?
1. throw unwinds the stack
public Order get(long id) {
return orders.findById(id)
.orElseThrow(() -> new OrderNotFoundException(id));
}
throw stops the method. Frames unwind until a matching catch or the thread’s default handler. Locals in abandoned frames are gone. Heap objects remain until they are unreachable.
The object I throw is a heap object like any other. It holds a stack trace captured at throw (roughly: fillInStackTrace). That trace is the story of where it was thrown, not where it is caught.
Memory sentence:
throwabandons frames until something catches. The stack trace is from the throw site.
2. throws is the checked contract
public String load(Path path) throws IOException {
return Files.readString(path);
}
throws IOException is required because readString is checked. For RuntimeException subtypes I may write throws as documentation. I usually do not; the type of the exception class is enough.
Overriding: a child method cannot add new checked exceptions the parent did not declare. It can throw fewer. That is substitution (Week 3): callers of the parent type must still compile.
interface Loader {
String load() throws IOException;
}
class CachedLoader implements Loader {
public String load() { // OK: no extra checked exceptions
return cached;
}
}
3. catch matching
Java picks the first catch whose type is assignment-compatible with the thrown object.
try {
load(path);
} catch (FileNotFoundException e) {
// more specific
} catch (IOException e) {
// the rest of I/O
}
Specific before general. catch (IOException e) before catch (FileNotFoundException e) does not compile: the second is unreachable.
Multi-catch when the handling is the same:
catch (IllegalArgumentException | IllegalStateException e) {
throw new BadRequestException(e.getMessage(), e);
}
The variable e is final in multi-catch.
Never catch (Exception e) { } empty. Almost never catch (Exception e) in the middle of a service. The right place for a wide catch is a boundary: servlet filter, @ControllerAdvice, message listener, thread pool wrapper — somewhere that can log, map, or retry.
4. Wrap with a cause
When I translate, I keep the original as the cause:
try {
return Files.readString(path);
} catch (IOException e) {
throw new UncheckedIOException("Failed to read " + path, e);
}
e is the cause. getCause() and the printed stack show both. Losing the cause is how production incidents become “something failed” with no root.
new OrderNotFoundException(id); // no cause — that is fine if this IS the root
new PaymentFailedException("stripe", e); // always pass e when wrapping
Rethrow the same object when I have not added meaning:
catch (IOException e) {
log.debug("retryable", e);
throw e; // same object, same trace
}
throw e vs throw new IOException(e): wrapping adds a frame and a type. Use a new type when the layer’s language changed (SQL → domain). Use the same e when I only logged.
5. Spring connection
orElseThrow(() -> new OrderNotFoundException(id))is the usual repository-to-service line.- Rest controllers should not have five
catchblocks. One advice class maps types. - Wrapping JDBC’s checked
SQLExceptionis whatJdbcTemplatealready did.
6. Common traps
Trap 1: catch (Exception e) { log.error(e); } without rethrow.
The caller thinks the operation succeeded.
Trap 2: e.printStackTrace() as the only handling.
Goes to stderr, not the app log, and the failure still needs a decision.
Trap 3: catch (FileNotFoundException | IOException e) — invalid, because FileNotFoundException is already an IOException.
Trap 4: Building a new exception without the cause: throw new RuntimeException(e.getMessage()).
The original type and trace are gone.
Trap 5: Catching NullPointerException as flow control.
Fix the null. Do not branch on NPE.
Practice Questions and Answers
Question 1
What is the difference between throw and throws?
Answer:
throw actually raises an exception object and starts unwinding. throws declares that a method may pass a checked exception to its caller. One is runtime control flow. The other is a compile-time contract.
Question 2
Why must the more specific catch come first?
Answer:
Matching is first-fit. A general catch (IOException e) would already handle FileNotFoundException, so a later specific catch would be dead code. The compiler rejects that.
Question 3
How do I wrap an exception without losing the root?
Answer:
Pass the original as the cause: new DomainException("payment failed", e). Logs and getCause() keep the chain. Do not copy only e.getMessage().
Question 4
Can an override add throws IOException if the parent has none?
Answer:
No. Callers using the parent type never declared that checked exception. Adding it would break substitution. The child can throw unchecked types, or catch and translate.
Question 5
When is catch (Exception e) acceptable?
Answer:
At a process boundary that must not kill a thread silently: a message listener, a scheduled job wrapper, @ControllerAdvice. Even there I log, map to a response or retry, and I do not swallow. Inside OrderService.place I catch the specific types I can compensate for, or I let it propagate.
Memory sentences
throwunwinds.throwsdeclares a checked contract.
Catch specific before general. Empty catch is a production bug.
Wrap with the cause. Do not keep only the message.