Why can only objects whose class extends Throwable be thrown or caught in Java, and what are the practical implications when designing custom exceptions?
answer
- throw/catch are typed to Throwable — compiler + JVM enforced
- Throwable gives message, cause (chaining), stack trace, suppressed
- Custom: extend Exception (checked) or RuntimeException (unchecked)
- Never extend Error/Throwable directly for app exceptions
- Always preserve the cause; provide (msg, cause) constructor
basics
~20 sThe language and JVM require that anything you throw or catch be a Throwable, because Throwable carries the machinery exceptions need — a message, a cause, and the stack trace. So your custom exceptions must extend Throwable (in practice, Exception or RuntimeException).
solid answer
~40 sJava restricts throw/catch to Throwable subclasses because Throwable defines the contract every thrown object needs: a detail message, an optional cause for chaining, and a captured stack trace (filled in at construction via fillInStackTrace). The compiler rejects throwing a non-Throwable, and the JVM's exception tables are typed in terms of Throwable. When designing custom exceptions you therefore extend Exception (checked) for recoverable, anticipatable conditions that should be in the API contract, or RuntimeException (unchecked) for programming errors or where you want to avoid forcing callers to handle them. You should preserve causes by passing the original throwable to the constructor (exception chaining) so the root cause isn't lost. Extending Error or Throwable directly for application exceptions is almost always wrong. A meaningful message and the right checked/unchecked choice are the main design levers.
code
java · 16 lines// Custom checked exception with standard constructors and cause preservation
public class ConfigLoadException extends Exception {
public ConfigLoadException(String message) { super(message); }
public ConfigLoadException(String message, Throwable cause) { super(message, cause); }
}
// Wrapping at a boundary: keep the cause so 'Caused by:' survives
try {
parse(file);
} catch (IOException e) {
throw new ConfigLoadException("failed to load " + file, e); // chaining
}
// Illegal: you cannot throw a non-Throwable
// throw "oops"; // compile error
// catch (String s) { ... } // compile errorgo deeper
Knows custom exceptions must extend an existing exception class (usually Exception or RuntimeException) and that you can't throw plain objects.
Explains that Throwable supplies message, cause, and stack trace, and chooses between extending Exception and RuntimeException with correct reasoning.
Designs robust custom exceptions: standard constructors, cause preservation/chaining, deliberate checked/unchecked choice, useful messages without leaking secrets.
Sets exception-design and error-handling policy across services (exception taxonomy, boundaries where you wrap/translate, performance trade-offs like fillInStackTrace, and avoiding exceptions as control flow), and understands the JVM-level rationale for the single-rooted hierarchy.
## The rule and where it comes from In Java, the `throw` statement and `catch` clause are *typed*: their operand must be a reference whose static type is `Throwable` or a subclass. `throw 42;` or `catch (String s)` are **compile errors**. At a lower level, the JVM associates each `try` region with an *exception table* whose handlers are keyed by `Throwable` subtypes, and the `athrow` bytecode expects a `Throwable` on the stack. So the restriction is enforced both by the compiler and baked into the JVM's design. ## Why `Throwable` specifically — what it provides The reason a thrown object *must* be a `Throwable` is that exception handling needs certain capabilities that `Throwable` defines and implements: - **A detail message** (`getMessage()`) — human-readable description of what went wrong. - **A cause** (`getCause()`, set via a constructor or `initCause`) — enabling *exception chaining*, so a low-level failure can be wrapped by a higher-level one without losing the original. - **A stack trace** — captured when the `Throwable` is constructed (via `fillInStackTrace()`), recording the call path so you can see *where* it happened. This is the single most valuable diagnostic feature. - **Suppressed exceptions** (`getSuppressed()`/`addSuppressed`) — used by try-with-resources to retain exceptions thrown while closing resources. An arbitrary `Object` has none of this, which is exactly why the language refuses to let you throw one. ## Designing custom exceptions — the practical choices Because you must extend the hierarchy, the design question is **which superclass**: ### Extend `Exception` (checked) Use for *anticipatable, recoverable* conditions you want to make part of the method's published contract, forcing callers to decide how to handle them. Example: a `InvalidConfigurationException` your loader can recover from by falling back to defaults. ```java public class InvalidConfigurationException extends Exception { public InvalidConfigurationException(String message, Throwable cause) { super(message, cause); // preserve the cause — chaining } } ``` ### Extend `RuntimeException` (unchecked) Use for *programming errors* or when you deliberately don't want to burden every caller with handling — common in modern frameworks (Spring's `DataAccessException` is unchecked). Example: a `DomainValidationException` that signals a precondition violation. ### Almost never extend `Error` or `Throwable` directly `Error` is reserved for JVM/environment problems; application code shouldn't masquerade as that. Extending `Throwable` directly is legal but pointless — you lose nothing by extending `Exception`/`RuntimeException` and you confuse readers. ## Best-practice design levers 1. **Provide the standard constructors** — `(String message)`, `(String message, Throwable cause)`, and often `(Throwable cause)` — so callers can chain. 2. **Preserve the cause.** When you catch and rethrow at a higher abstraction, pass the original: `throw new ServiceException("load failed", e);`. Swallowing the cause destroys the root-cause stack trace. 3. **Write a useful message** including the offending values (but never secrets/PII). 4. **Choose checked vs unchecked deliberately**, knowing checked exceptions couple callers and interact poorly with lambdas, while unchecked exceptions are silent in signatures. 5. **Consider performance edges.** Stack-trace capture (`fillInStackTrace`) is relatively expensive. For exceptions thrown on hot paths purely as control flow, you *can* override `fillInStackTrace()` to skip capture (a deliberate, documented optimization) — but using exceptions for ordinary control flow is itself a smell. 6. **Keep exceptions immutable and self-describing**; avoid carrying mutable state. ## Why this all ties back to the hierarchy The single-rooted `Throwable` tree is what makes `catch (IOException e)` work polymorphically (it catches subclasses too), what lets a top-level handler use `catch (Exception e)`, and what guarantees every thrown thing carries a stack trace. Your custom types inherit all of that *for free* by extending the right node — which is the whole point of being forced to extend `Throwable`.
- What is exception chaining and why does it matter?Chaining links a higher-level exception to the lower-level one that caused it via the cause field (a (message, cause) constructor or initCause). It matters because rethrowing at a higher abstraction without the cause discards the original stack trace and root-cause information; with chaining, the printed trace shows 'Caused by:' all the way down, making diagnosis possible.
- Why might someone override fillInStackTrace() in a custom exception, and what's the risk?fillInStackTrace() captures the stack trace at construction, which is relatively costly. For exceptions thrown extremely frequently as a control-flow signal, overriding it to skip capture improves performance. The risk is losing diagnostic information and encouraging exceptions-as-control-flow, which is generally an anti-pattern; do it only deliberately and document it.
saying these in an interview costs you the question
- Thinking you can throw arbitrary objects (strings, ints) like some languages allow
- Extending Throwable or Error directly for ordinary application exceptions
- Catching and rethrowing without passing the original cause (losing the stack trace)
- Believing the throw/catch typing is only a compiler nicety and the JVM is untyped — the JVM's exception tables are typed too
- Overusing checked exceptions, or using exceptions for normal control flow