Detailed explanation
Exception chaining in Java means associating one exception with another as its cause. Typically, a method catches a lower-level exception and throws a higher-level exception that describes the failure in terms meaningful to its callers. The original exception remains attached rather than being discarded. In your example, an IOException from parseFile() becomes the cause of a RuntimeException with the message "Failed to process file". This adds context while retaining the evidence needed to understand the underlying failure.
The usual mechanism is a constructor that accepts a cause, such as new RuntimeException("Failed to process file", e). The wrapper and the cause are separate Throwable objects, each retaining its own stack trace. Calling getCause() on the wrapper returns e; calling getMessage() returns the wrapper's detail message, not the cause's message. Standard printStackTrace() output includes the wrapper and follows its cause chain, displaying "Caused by:" sections. It may abbreviate shared stack frames with "... N more", so the printed trace preserves the relationship without repeating every frame. For exception types without a suitable constructor, initCause(e) can attach a cause if it has not already been initialized.
Chaining matters at abstraction boundaries. For example, a repository might translate SQLException into a custom DataAccessException, allowing callers to handle a persistence-level failure without depending directly on JDBC exception types. The chain can contain multiple levels as the failure moves through application layers. Java does not provide getRootCause() on Throwable; finding the deepest cause normally requires following getCause() until it returns null. Whether the wrapper is checked or unchecked depends on its type, not on the type of its cause.
Interviewers look for more than the phrase "wrapping an exception". Explain that chaining combines meaningful context with diagnostic preservation, and that wrappers should represent useful abstraction changes rather than add noise at every method. Also distinguish chaining from rethrowing: throw e propagates the same exception object, whereas throwing a new exception with e as its cause creates a new outer failure. Neither approach automatically recovers from the error; recovery requires separate handling logic.
Key points to remember
- Pass the original exception to a cause-aware constructor, usually alongside a contextual message.
- getCause() returns the immediate cause, which may itself have another cause.
- The wrapper and cause retain separate stack traces; printStackTrace() displays the chain.
- Use chaining to translate failures across abstraction boundaries without losing diagnostic details.
- The wrapper's class determines whether callers face checked-exception handling requirements.
Common mistakes candidates make
- Throwing new RuntimeException(e.getMessage()) and losing the original exception object, type, and stack trace.
- Assuming getMessage() returns the complete cause chain or that Throwable provides getRootCause().
- Wrapping exceptions at every layer without adding useful context or a meaningful abstraction.
Likely follow-up questions
When can you use initCause()?
Use initCause() when the exception has no suitable cause-aware constructor and its cause has not already been initialized. A second initialization throws IllegalStateException, and setting an exception as its own cause throws IllegalArgumentException.
How do you support chaining in a custom exception?
Provide a constructor such as public DataAccessException(String message, Throwable cause) { super(message, cause); }. Extend Exception for a checked exception or RuntimeException for an unchecked exception, according to the API contract.
How are causes different from suppressed exceptions?
A cause explains why the outer exception occurred. A suppressed exception is a secondary failure retained alongside the primary failure, such as a resource-closing error during try-with-resources; access these through getSuppressed().
Interview tip: Define chaining, walk through the IOException-to-RuntimeException example, and explain how getCause() preserves the original failure.
