Question 29 · Section 7

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

Language versions: English Russian Ukrainian

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

  1. Input validation - IllegalArgumentException("age must be positive") doesn’t need a cause
  2. Programmer errors - NullPointerException is not wrapped, it’s fixed
  3. Deep nesting 10+ - stack trace becomes unreadable, use truncation
  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

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 cause node to see the entire error tree
  • ELK/Splunk - cause as 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)
  • Throwable stores private Throwable cause = this - set via constructor or initCause()
  • 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() or ExceptionUtils.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 cause and initCause()
  • [[16. What does the printStackTrace() method do]] - printing entire chain with Caused by: