When designing a method, how do you decide whether to catch a checked exception locally or declare it in throws?
answer
- Handle where you have context to act
- Catch => recover / retry / default / translate
- Declare => let a smarter layer decide
- Translate low-level to domain exception, keep the cause
- Never swallow to silence the compiler
basics
~10 sCatch it where you can actually do something useful — recover, retry, use a default, or add context. Otherwise declare it and let a higher layer that has the context to decide handle it.
solid answer
~50 sThe deciding question is: 'Can this layer meaningfully handle the failure?' If yes — retry, fall back to a default, log-and-continue, or translate it into a domain-specific exception with added context — then catch it here. If this layer can't sensibly decide what to do, declare it (or wrap it) and let it bubble to a layer that can. A common anti-pattern is catching a checked exception just to silence the compiler (empty catch or catch-and-log-and-swallow), which hides real failures. Two refinements: (1) catch low and re-throw as a meaningful abstraction so callers don't depend on implementation details (exception translation, often wrapping the cause); (2) avoid declaring overly broad types like throws Exception, which forces callers into coarse handling and leaks nothing useful. The goal is that each failure is handled exactly once, at the layer with enough context to react correctly.
code
java · 21 lines// Translate low-level checked exception into a domain exception,
// preserving the original as the cause for debugging.
User load(long id) {
try {
return jdbc.queryForObject(SQL, mapper, id);
} catch (SQLException e) {
// This layer can't decide the business reaction, but it can
// hide the JDBC detail and add context for callers.
throw new UserRepositoryException("failed to load user " + id, e);
}
}
// Recover locally when this layer DOES have context:
Properties loadConfig() {
try {
return readFrom("app.properties");
} catch (IOException e) {
log.warn("config missing, using defaults", e);
return defaults(); // meaningful recovery -> catch here
}
}go deeper
Knows you can either catch or declare, and that empty catch blocks are bad, even if not yet fluent in layering decisions.
Chooses catch vs. declare based on whether the layer can recover, and avoids obvious anti-patterns like swallowing or throws Exception.
Applies exception translation with cause chaining, designs stable failure abstractions per layer, and reasons about checked vs. unchecked tradeoffs.
Sets codebase-wide error-handling strategy (where failures are handled, checked vs. unchecked policy, global handlers), balancing API clarity, boilerplate, and operability.
## The core question When a method body can produce a checked exception, the compiler forces a choice: **catch** it or **declare** it (`throws`). Beyond satisfying the compiler, this is a *design* decision. The guiding principle: **handle a failure at the layer that has enough context and ability to do something meaningful about it.** ## When to CATCH locally Catch the exception in this method when this layer can take a genuine action: - **Recover / retry**: e.g. a transient network read fails — retry a few times. - **Fall back to a default**: config file missing — use built-in defaults. - **Log and continue**: one item in a batch fails — record it, keep processing the rest. - **Translate to a meaningful exception**: convert a low-level `SQLException` into a domain `UserRepositoryException`, attaching context, so callers don't couple to JDBC. - **Clean up**: although `try-with-resources`/`finally` handle cleanup, sometimes catching is needed to release something and rethrow. ## When to DECLARE (let it propagate) Declare the exception (add it to `throws`) when **this layer cannot sensibly decide** what to do. A deep utility method usually has no idea whether a missing file should be fatal, retried, or defaulted — only a higher layer with business context knows. Declaring pushes the decision to where the context lives. ## Exception translation (wrapping) A powerful middle path is to **catch low and re-throw as a more meaningful type**, preserving the original as the *cause*: ```java try { return jdbc.query(...); } catch (SQLException e) { throw new UserRepositoryException("failed to load user " + id, e); // e is the cause } ``` This hides the implementation detail (`SQLException`) from callers, gives them a stable abstraction, and keeps the stack trace via the chained cause. Whether the new type is checked or unchecked is itself a design choice (see below). ## Anti-patterns to avoid - **Swallowing**: `catch (IOException e) {}` — silences the compiler and the failure. Almost always a bug. - **Catch-log-continue when you can't actually continue**: logging then proceeding with corrupt state. - **`throws Exception` everywhere**: declaring the broadest type forces every caller into coarse handling and erases which failures are actually possible. - **Catching `Exception`/`Throwable` indiscriminately**: can swallow programming bugs (unchecked) and even `Error`s you shouldn't handle. ## Checked vs. unchecked as a strategy The whole catch-or-declare burden exists only for **checked** exceptions. Many modern Java codebases and frameworks (e.g. Spring) lean toward **unchecked** exceptions to avoid `throws` clutter and pass-through boilerplate, choosing to handle failures centrally (e.g. a global handler). Checked exceptions shine when the caller *must* be reminded to handle a recoverable condition. Knowing when each is appropriate is the senior-level judgment this question targets. ## A decision checklist 1. Can *this* layer recover or make a real decision? → **catch**. 2. Does the low-level type leak implementation detail callers shouldn't see? → **catch and translate** (wrap with cause). 3. Otherwise → **declare** (specific types, not `Exception`). 4. Never catch merely to silence the compiler. ## Deriving the answer The whole thing reduces to: *handle where there's context to handle; propagate (or translate) where there isn't; never swallow.* The `throws` clause is the tool for propagation; try/catch is the tool for handling; wrapping is the tool for changing the abstraction level on the way up.
- What is exception translation and why preserve the cause?Catching a low-level exception and rethrowing a higher-level, domain-meaningful one. You pass the original as the cause (new XException(msg, e)) so the full stack trace and root cause stay available for debugging while callers depend only on the stable abstraction.
- Why do some frameworks prefer unchecked exceptions?To remove throws-clause clutter and forced try/catch boilerplate across layers, handling failures centrally (e.g. a global exception handler). The tradeoff is that the compiler no longer reminds callers to handle them.
saying these in an interview costs you the question
- Empty catch blocks to satisfy the compiler
- Declaring throws Exception on everything
- Catching and logging then continuing with broken state
- Catching Throwable/Exception broadly and swallowing bugs
- Wrapping but dropping the original cause (losing the stack trace)