What is exception chaining?
4. In signal exceptions - lightweight signal exceptions without context (FastException) 5. At microservice boundaries - pass DTO with error code, don't serialize the entire chain
Junior Level
Definition
Exception chaining - a mechanism linking several exceptions together, where each exception points to its cause (reason).
Example
try {
// Code that can throw SQLException
connection.executeQuery("SELECT * FROM users");
} catch (SQLException e) {
// Wrap in our exception, preserving the cause
throw new DataAccessException("Failed to query database", e);
// cause ^
}
How to get the cause
try {
service.process();
} catch (DataAccessException e) {
Throwable cause = e.getCause(); // Original SQLException
System.out.println("Root cause: " + cause.getMessage());
}
Why it’s needed
- Preserve stack trace - original error is not lost
- Add context - each layer adds its own information
- Hide details - client doesn’t depend on
SQLException
When NOT to use exception chaining
- Input validation -
IllegalArgumentException("age must be positive")doesn’t need a cause - Programmer errors -
NullPointerExceptionis not wrapped, it’s fixed - Deep nesting 10+ - stack trace becomes unreadable, use truncation
- In signal exceptions - lightweight signal exceptions without context (
FastException) - At microservice boundaries - pass DTO with error code, don’t serialize the entire chain
Middle Level
Cause field in Throwable
Inside Throwable there is a field private Throwable cause = this;.
The default value this means the cause has not yet been set.
Method initCause(Throwable) or constructor super(message, cause) sets this field.
Limitation: cause can be set only once. Repeated initCause -> IllegalStateException.
Architectural levels of chains
Layer Web: catch (DataAccessException e) -> 500 Internal Server Error
^
Layer Service: catch (SQLException e) -> throw new DataAccessException("...", e)
^
Layer DAO: catch (SQLException e) -> propagates further
^
JDBC Driver: throws SQLException
At each step cause is preserved, so in logs you can trace to the real cause.
Finding Root Cause
// Apache Commons Lang
Throwable root = ExceptionUtils.getRootCause(e);
// Guava
Throwable root = Throwables.getRootCause(e);
// Manually
Throwable t = e;
while (t.getCause() != null) {
t = t.getCause();
}
// t - root cause
Suppressed vs Cause
- Cause - “why did I fail” (root cause)
- Suppressed - “what else went wrong while I was failing” (usually on resource closing)
Senior Level
Stack Trace Bloat
Deep chains (10+ levels) create huge text logs. They load I/O and take up disk space.
Optimization: when logging, configure stack depth limits or use JSON logs, where stack trace is a separate indexable field.
Object Allocation and GC
Each link in the chain - an object in the heap. In Highload systems, frequent errors with deep chains can cause GC load.
Circular Dependency
JVM prevents chain cycling:
e1.initCause(e2);
e2.initCause(e1); // IllegalArgumentException
Serialization
When transmitting a chain over a network (RMI/gRPC), all links must be serializable. If one of the nested exceptions is a custom class that the client doesn’t have - deserialization of the entire chain fails.
Solution: at microservice boundaries, pass only DTO with message and error code.
Log Analysis
Make sure the log format includes %ex (Logback) or similar marker revealing cause. Some old configurations only write the top exception’s message.
Diagnostics
- IntelliJ Debugger - expand
causenode to see the entire error tree - ELK/Splunk -
causeas a separate field for indexing - Metrics - count chain depth via
getSuppressed().length - Root Cause Tracking - each chain link should be logged with Trace ID
Interview Cheat Sheet
Must know:
- Exception chaining - mechanism of linking exceptions via
cause(reason) Throwablestoresprivate Throwable cause = this- set via constructor orinitCause()- Cause can be set only once - repeated
initCause()->IllegalStateException - Each architectural layer adds its own context: DAO -> Service -> Controller
getCause()gets the original exception,getRootCause()- root cause- Cause vs Suppressed: cause = “why did I fail”, suppressed = “what else went wrong while I was failing”
- Deep chains (10+) create Stack Trace Bloat - huge logs, GC load
Frequent follow-up questions:
- How to find root cause? - Loop
while (t.getCause() != null) t = t.getCause()orExceptionUtils.getRootCause(e) - How does cause differ from suppressed? - Cause - root cause, suppressed - parallel errors (usually on close)
- Can you cycle a chain? - No, JVM prevents it:
e1.initCause(e2); e2.initCause(e1)->IllegalArgumentException - Serialization problems with chain? - All links must be Serializable; at microservice boundaries pass DTO
Red flags (NOT to say):
- “Can call
initCause()multiple times” - only once - “Chaining and suppressed are the same” - no, cause = root cause, suppressed = parallel errors
- “Chain of 20 exceptions is fine” - Stack Trace Bloat, I/O and GC load
- “I serialize the entire chain over the network” - DTO with error code instead of serialization
Related topics:
- [[18. What is exception wrapping (wrapping)]] - creating chain through wrapping
- [[23. What are suppressed exceptions]] - suppressed vs cause
- [[15. What is a stack trace]] - reading chain stack trace
- [[6. What is Throwable]] - field
causeandinitCause() - [[16. What does the printStackTrace() method do]] - printing entire chain with
Caused by: