Deciding what happens when things go wrong β which is most of what production code does.
π Where this lives: read any battle-tested library and you will find that the error paths outnumber the success path, often by a lot. That is not defensive paranoia; it is where the real engineering is. Any competent programmer can write the code that works when everything is available and valid. Deciding what the system does when the payment gateway times out after charging the customer is the part that separates software that survives production from software that merely demonstrates. Search "error handling is the majority of production code".
What an exception is for
AN EXCEPTION separates the DETECTION of a problem from its
HANDLING. The code that notices the problem is rarely the code
that knows what to do about it, and exceptions are the mechanism
for passing that decision up to whoever does.
WHY NOT RETURN CODES? The classic comparison:
int result = withdraw(account, amount);
if (result == -1) β¦ // insufficient funds
else if (result == -2) β¦ // account frozen
Β· the error can be IGNORED silently β the commonest cause of
corrupt state, because ignoring it compiles cleanly
Β· the error path and the value share one return channel, so a
legitimate β1 is ambiguous
Β· every caller must check at every level, and the checking
obscures the logic
WITH EXCEPTIONS the failure cannot be silently discarded, and
the intervening layers need no error-handling code at all.
(Note that some modern languages return an explicit
Result/Either type instead, which recovers the "cannot be
ignored" property without unwinding the stack β a legitimate
third position rather than a return to error codes.)
THE THREE CATEGORIES, and the distinction that governs
everything else:
1. PROGRAMMING ERRORS (bugs) β a null dereference, an index out
of bounds, an illegal argument from internal code.
THESE SHOULD NOT BE CAUGHT AND HANDLED. They mean the code is
wrong; the correct response is to fail loudly so the bug is
found and fixed. Catching them hides defects.
2. EXPECTED DOMAIN FAILURES β insufficient funds, an illegal
status transition, a duplicate application, an unpaid fee.
THESE ARE PART OF THE SPECIFICATION. They come from the use
case extensions of ACtE0804 and belong in the method
signature, because a caller must handle them.
3. ENVIRONMENTAL FAILURES β a network timeout, a full disk, a
database unavailable, an external service down.
THESE ARE TRANSIENT AND EXPECTED IN AGGREGATE. The response
is the retry/timeout/circuit-breaker machinery of the
inter-organisational topic, not a domain rule.
CHECKED vs UNCHECKED (Java's distinction, and the argument
generalises):
CHECKED the compiler forces the caller to handle or
declare it. Right for category 2 β a recoverable
condition the caller must think about.
UNCHECKED no compiler obligation. Right for category 1 β
nobody should be handling a bug β and often used
for category 3, where the handling is centralised
rather than local.
THE PRACTICAL CRITERION: is there something USEFUL the
immediate caller could do about it? If yes, checked. If the
only honest response is "abort and report", unchecked, handled
once at the boundary.
The rules that keep error handling honest
1. NEVER SWALLOW AN EXCEPTION.
β try { save(app); } catch (Exception e) { }
This is the single worst line in software. The operation
failed, nobody knows, and the system continues believing it
succeeded. If you genuinely intend to ignore a failure, SAY SO
IN A COMMENT AND LOG IT:
β catch (SmsException e) {
// Notification failure must not roll back an issued
// licence (UC-05 ext. 7a). Logged for follow-up.
log.warn("SMS notification failed for {}", licenceNo, e);
}
The comment cites the requirement, so the next reader knows
this is a decision rather than an oversight.
2. CATCH SPECIFICALLY, NOT BROADLY.
`catch (Exception e)` catches the NullPointerException from
your own bug alongside the timeout you meant to handle, and
treats them identically. Catch the narrowest type that
matches what you can actually handle.
3. DO NOT USE EXCEPTIONS FOR CONTROL FLOW.
β try { return map.get(k); } catch (NotFound e) { return
DEFAULT; }
β return map.getOrDefault(k, DEFAULT);
Exceptions are expensive (stack capture) and, more
importantly, they obscure the normal path.
4. FAIL FAST β validate at the boundary.
Check preconditions on entry, so an invalid value is
rejected where it entered rather than causing a confusing
failure five layers deeper. This is the constructor
validation of the earlier topic, generalised.
5. PRESERVE THE CAUSE WHEN WRAPPING.
β catch (SQLException e) {
throw new RepositoryException("save failed");
} // the original stack trace is GONE
β throw new RepositoryException("save failed", e);
Losing the cause turns a five-minute diagnosis into an
afternoon.
6. TRANSLATE AT LAYER BOUNDARIES.
The domain must not throw `SQLException` β that would make
every caller depend on the persistence technology, breaking
the dependency rule from ACtE0805. The repository
implementation catches the technology-specific exception and
throws the domain's own:
catch (SQLException e) {
throw new ApplicationNotFound(no, e);
}
7. CLEAN UP DETERMINISTICALLY.
Use the language's scoped-resource construct β
try-with-resources, `using`, `with`, `defer` β rather than
a finally block you might forget. A resource freed in
`finally` is correct; one freed only on the success path is
a leak that appears only under failure, i.e. exactly when
the system is already struggling.
8. AN EXCEPTION MESSAGE MUST BE ACTIONABLE.
β "Invalid input"
β "fee 12000 exceeds the maximum 10000 for category B"
Include the offending value and the constraint. The person
reading it at 2 a.m. has only this string.
BUT NEVER PUT SECRETS OR PERSONAL DATA IN A MESSAGE that
reaches a user or a shared log β a stack trace shown to an
end user is an information-disclosure defect as well as a
poor experience.
9. DISTINGUISH WHAT THE USER SEES FROM WHAT IS LOGGED.
the LOG gets the type, the message, the cause chain, the
stack, and a correlation id
the USER gets a plain explanation of what happened and what
to do next, plus that correlation id so support can find
the log entry
THE CORRELATION ID IS THE WHOLE TRICK: it links a user's
complaint to the exact log record without exposing anything.
Worked: error handling through the layers
ONE OPERATION, and the error decision at each layer. This is the
synthesis of the whole section.
ββ DOMAIN LAYER β states the rules, knows nothing about users
public Licence issue(LicenceNo no, OfficerId officer)
throws NotIssuableException,
SeparationOfDutiesException {
assertTransition(Status.ISSUED);
if (!isPaid())
throw new NotIssuableException(
"application " + applicationNo + " has an unpaid fee of "
+ feeDue());
if (officer.equals(reviewedBy))
throw new SeparationOfDutiesException(
"officer " + officer + " reviewed this application "
+ "and may not also issue it");
β¦
}
Β· CHECKED exceptions, because these are category 2 β the
caller must decide
Β· messages name the offending values
Β· both exceptions come straight from the operation contract's
preconditions
ββ INFRASTRUCTURE β translates technology into domain terms
public Application find(String no) throws ApplicationNotFound {
try {
return jdbc.queryForObject(SQL, mapper, no);
} catch (EmptyResultDataAccessException e) {
throw new ApplicationNotFound(no, e); // translate
} catch (DataAccessException e) {
// Category 3: transient. Wrapped unchecked so the
// domain has no obligation to know the database exists.
throw new RepositoryUnavailable("looking up " + no, e);
}
}
Β· the cause is preserved in both branches
Β· the domain never sees a database exception
ββ APPLICATION LAYER β owns the transaction, adds nothing else
@Transactional
public Licence handle(String no, OfficerId officer)
throws ApplicationNotFound, NotIssuableException,
SeparationOfDutiesException {
Application app = repository.find(no);
LicenceNo licNo = pool.allocate();
Licence lic = app.issue(licNo, officer);
repository.save(app);
events.publish(new LicenceIssued(lic));
return lic;
}
Β· CATCHES NOTHING. Every exception propagates, and the
transaction rolls back β which is exactly the desired
behaviour and requires no code to achieve.
Β· THE ABSENCE OF A try/catch HERE IS THE DESIGN DECISION.
A catch block would have to decide what the user sees, and
this layer does not know whether the caller is a web page,
a mobile app or a batch job.
ββ PRESENTATION LAYER β the ONE place that maps failures to
responses
@ExceptionHandler(NotIssuableException.class)
ResponseEntity<ApiError> onNotIssuable(NotIssuableException e) {
return status(409).body(
new ApiError("CANNOT_ISSUE", e.getMessage(), traceId()));
}
@ExceptionHandler(ApplicationNotFound.class) β 404
@ExceptionHandler(SeparationOfDutiesException.class) β 403
@ExceptionHandler(NumberPoolExhaustedException.class) {
log.error("licence number pool exhausted", e); // ALERT
return status(503).body(new ApiError(
"TEMPORARILY_UNAVAILABLE",
"Licences cannot be issued right now. "
+ "The administrator has been notified.", traceId()));
}
@ExceptionHandler(Exception.class) β 500
log.error("unhandled failure, trace {}", traceId(), e);
// The user gets NO stack trace and no internal detail.
return status(500).body(new ApiError(
"INTERNAL_ERROR",
"Something went wrong. Quote reference " + traceId()
+ " to support.", traceId()));
READ WHAT THIS ACHIEVES:
Β· ONE place decides HTTP status codes, so they are consistent
Β· the domain is free of presentation concerns and the
presentation is free of business rules
Β· every failure is logged once, with a trace id, and the user
receives that id and nothing sensitive
Β· the pool-exhausted case is a 503 with an alert, not a 500 β
because it is operational, and someone must act
Β· the catch-all guarantees that an unforeseen bug produces a
clean response rather than a stack trace in a browser
THE PATTERN, stated generally: THROW WHERE THE PROBLEM IS
DETECTED, TRANSLATE AT LAYER BOUNDARIES, AND HANDLE ONCE AT
THE OUTERMOST EDGE. Intermediate layers catch nothing, which is
why they stay readable.
AND THE CASE THIS STRUCTURE STILL DOES NOT SOLVE β worth being
honest about. UC-05 extension 4a: the payment gateway times out
AFTER charging the applicant. No exception handler can fix
that, because the failure is that two systems now disagree.
It needs a RECONCILIATION process β the compensating action of
the saga pattern from the control styles topic. SOME ERRORS ARE
NOT HANDLED IN CODE AT ALL; they are handled by a business
process that the software must support.
The most instructive part of the worked example is the layer that contains no error handling at all. The application layer catches nothing because it cannot know whether the caller is a web page, a mobile app or a batch job β so deciding what the user sees is not its business. Restraint is the design decision there.
π Go further: the operational counterpart of all this is structured logging with correlation ids. Every request is assigned an id at the edge, that id travels with the work through every layer and every service call, and every log line carries it as a field rather than as prose. When a user quotes a reference number, one query reconstructs the entire path of that request across the whole system β which is what makes debugging a distributed failure tractable at all, and it is the direct answer to the "no single stack trace" problem the event-driven control style creates. Search "correlation id distributed tracing structured logging".
π‘ Exam angle: explain what exceptions are for β separating detection from handling β and compare them with return codes. Distinguish the three categories (programming errors, expected domain failures, environmental failures) and say how each should be treated, noting that programming errors should not be caught. Distinguish checked from unchecked exceptions with the criterion for choosing. The rules most likely to be examined: never swallow an exception, catch specifically, do not use exceptions for control flow, preserve the cause when wrapping, translate at layer boundaries, and separate what the user sees from what is logged. Be ready to describe error handling across the layers of a system.
Syllabus points
Exceptions and error handling
Create a free account to tick topics off, take notes as you read, watch the video lessons and get a day-by-day study plan built around your exam date.
Related topics in Object-Oriented Design Implementation