What roles do the detail message and the cause play in a custom exception, and how does Throwable manage them?
answer
- Message = what (getMessage); cause = why (getCause)
- Set via super(message)/super(message,cause)/super(cause)
- initCause() only once, else IllegalStateException
- 'Caused by:' walks the whole chain to root
- Never swallow the cause when wrapping
basics
~20 sThe detail message is human-readable text describing what went wrong, returned by getMessage(). The cause is the original exception that triggered this one, returned by getCause(). Throwable stores and prints both; you set them via super(...).
solid answer
~50 sThrowable maintains two pieces of diagnostic state that your custom exception inherits: the detail message and the cause. The detail message is a String describing the specific failure; you set it via super(message) and read it via getMessage(), and it appears beside the class name at the top of the stack trace. The cause is another Throwable representing the underlying exception that led to this one; you set it via super(message, cause) or super(cause), or later via initCause(), and read it with getCause(). Chaining a cause lets you translate a low-level exception (like SQLException) into a domain exception without discarding the root failure: the printed trace shows your exception and then a 'Caused by:' section for each link in the chain. Because Throwable already implements all of this storage, formatting, and traversal, your custom class should delegate to super rather than reinvent it. Good messages are specific and contextual; the cause preserves the why.
code
java · 11 linespublic class RepoException extends RuntimeException {
public RepoException(String message, Throwable cause) { super(message, cause); }
}
try {
jdbc.query(sql);
} catch (SQLException e) {
// message = what, cause = why; preserves the root cause
throw new RepoException("load failed for id " + id, e);
}
// Trace shows: RepoException: load failed ... Caused by: java.sql.SQLException: ...go deeper
Knows getMessage() returns the text passed to the constructor and that exceptions print a message.
Can set and read both message and cause and explain chaining at a basic level with super(message, cause).
Explains the message-vs-cause roles, the initCause one-shot rule, the Caused-by trace traversal, and why translating low-level into domain exceptions preserves the root cause.
Defines messaging/observability standards (contextual, secret-free messages; consistent wrapping at layer boundaries) and ensures traces remain actionable across distributed systems.
## Two distinct pieces of state A `Throwable` (and therefore every exception, including your custom ones) carries two key diagnostic values: 1. **The detail message** — a `String` describing *what* went wrong, e.g. `"No order with id 42"`. 2. **The cause** — another `Throwable` describing *why* it went wrong at a lower level, e.g. the `SQLException` that was caught. There's also the **stack trace** (the captured call stack), but the message and cause are the two you actively set. ## The detail message - **How it's set:** by passing a `String` to a constructor that forwards to `super(message)`. - **How it's read:** `getMessage()` returns exactly what you stored; `getLocalizedMessage()` returns the same by default but can be overridden for i18n. - **Where it shows:** the first line of a printed stack trace is `FullyQualifiedClassName: message`. So a clear message is the first thing a debugger sees. - **Good practice:** make messages **specific and contextual** — include the offending value or id (`"timeout after 5000ms calling billing-service"`), not vague text like `"error"`. Avoid leaking secrets or PII. ## The cause and exception chaining The **cause** captures the original exception that triggered the current one, a technique called **exception chaining** or **wrapping**. - **Why chain?** Lower layers throw implementation-specific exceptions (`SQLException`, `IOException`). Higher layers want to expose a clean, domain-meaningful type (`OrderNotFoundException`) without leaking the lower type. Chaining lets you do the translation *and* keep the original failure for debugging. - **How it's set:** - via constructor: `super(message, cause)` or `super(cause)`; - or after construction: `initCause(Throwable)` — but only **once**, and only if a cause wasn't already supplied (otherwise it throws `IllegalStateException`). - **How it's read:** `getCause()` returns the next exception down the chain (or `null`). - **How it's displayed:** `printStackTrace()` walks the whole chain, printing the top exception and then a `Caused by: ...` block for each link, down to the root cause. This is how you trace a high-level failure back to, say, a connection timeout. ### Example ```java try { jdbc.query(...); } catch (SQLException e) { throw new OrderNotFoundException("No order " + id, e); // chain the SQLException } ``` Resulting trace (abridged): ``` OrderNotFoundException: No order 42 at ... Caused by: java.sql.SQLException: connection reset at ... ``` ## Throwable does the heavy lifting The critical insight is that **`Throwable` already implements** the storage, retrieval, formatting, and chain-traversal for both message and cause: - `getMessage()`, `getCause()`, `initCause()`, `printStackTrace()`, `getStackTrace()` are all inherited. - Therefore your custom exception should **delegate to `super(...)`** for message and cause and **not** keep duplicate fields or override these methods, except for legitimate reasons (e.g. overriding `getMessage()` to compose a message from custom fields). ## Common mistakes - **Swallowing the cause:** `throw new OrderNotFoundException("No order " + id);` inside a `catch (SQLException e)` block — the root cause is lost. Always pass `e`. - **Double-setting a cause:** calling `initCause()` when a cause was already passed to the constructor → `IllegalStateException`. - **Vague messages:** `"failure"` tells the next engineer nothing; include context. - **Logging *and* rethrowing with the cause:** can produce duplicate stack traces; pick one place to log. ## Summary - **Message** = what (human-readable, `getMessage()`, set via `super(message)`). - **Cause** = why at a lower level (`getCause()`, set via `super(message, cause)`/`super(cause)`/`initCause()`). - **Chaining** preserves the root cause across abstraction boundaries; the `Caused by:` trace makes failures debuggable. - **Delegate to `Throwable`** — it already manages all of this.
- What happens if you call initCause() twice or after the constructor already set a cause?It throws IllegalStateException. A cause can be set exactly once — either through a cause-accepting constructor or a single initCause() call when no cause was previously set.
- How do you see the full chain of causes?printStackTrace() (or logging frameworks) walks getCause() recursively and prints a 'Caused by:' section for each link down to the root cause.
saying these in an interview costs you the question
- Wrapping an exception but dropping its cause, losing the root failure
- Calling initCause after passing a cause to the constructor (throws IllegalStateException)
- Putting secrets/PII in the detail message
- Keeping your own message/cause fields instead of delegating to Throwable
- Vague messages like 'error' with no context