Question 19 · Section 7

What is exception wrapping (wrapping)?

Chain of causality in the log will look like:

Language versions: English Russian Ukrainian

Junior Level

Definition

Wrapping - creating a new exception that contains the original exception as a cause.

Chain of causality - a sequence of exceptions where each subsequent one wraps the previous one. Imagine a matryoshka doll: outer exception is the service layer, inside it - database exception, and even deeper - network error. When you see a stack trace with wrapping, JVM prints the entire chain from outer to deepest (root cause).

Example

try {
    // Code that can throw SQLException
    connection.executeQuery("SELECT * FROM users");
} catch (SQLException e) {
    // Wrap in our exception
    throw new DataAccessException("Failed to query users", e);
    //                                                           ^--- cause
}

Chain of causality in the log will look like:

DataAccessException: Failed to query users
    at UserService.getUsers(UserService.java:25)
Caused by: SQLException: Connection timeout
    at DBConnection.executeQuery(DBConnection.java:42)

The Caused by: line - transition to the next link in the chain.

Why wrap

  1. Add context: "Failed to query users table for active users" is clearer than "Connection timeout"
  2. Hide implementation details: business logic shouldn’t know about SQLException
  3. Preserve cause: e is passed - original error’s stack trace is not lost
  4. Unified hierarchy: all service exceptions - subtypes of ServiceException

When NOT to use wrapping

  1. Input validation - IllegalArgumentException doesn’t need wrapping. This is a programming error, it needs to be fixed, not wrapped
  2. Programmer errors - NullPointerException, IndexOutOfBoundsException should not be caught and wrapped. These exceptions indicate a bug - let them reach the global handler
  3. Too deep nesting - if already 5+ levels of cause, don’t add another without good reason. Chain of 10 exceptions = unreadable stack trace
  4. Timeouts and retry - on automatic retry, don’t wrap each attempt, wrap only the final failure. Otherwise you get: RetryException -> TimeoutException -> TimeoutException -> TimeoutException -> SocketException
  5. Performance-critical code - each wrapped exception = new object + fillInStackTrace(). In hot-path this is noticeable overhead
  6. “Wrap-a-wrap” anti-pattern - don’t wrap an exception that is already a wrapper of the same abstraction level: ```java // BAD - ServiceException and DataAccessException are at the same level, no point try { repo.findUser(id); } catch (DataAccessException e) { throw new ServiceException(“User lookup failed”, e); // DataAccessException is already abstract enough, wrapping is pointless }

// GOOD - wrap only when transitioning between abstraction levels // DAO layer (SQLException) -> Service layer (DataAccessException) -> Controller (ServiceException)

7. **Propagating without adding context** - if wrapper adds no new message or metadata, it's noise:
```java
// Pointless - same message and cause
throw new MyException(e.getMessage(), e);

How to get the cause

try {
    service.process();
} catch (DataAccessException e) {
    Throwable cause = e.getCause(); // Original SQLException
    System.out.println("Root cause: " + cause.getMessage());
}

Middle Level

How it’s structured in Throwable

The Throwable class has a field private Throwable cause = this;.

When passed to constructor super(cause), initCause() is called.

Important: initCause() can only be called once. Repeated call - IllegalStateException.

Exception Translation in clean architecture

Wrapping - changing the level of abstraction:

// Bad - client depends on Stripe
public interface PaymentService {
    void charge(Card card) throws StripeException;
}

// Good - client depends on domain
public interface PaymentService {
    void charge(Card card) throws PaymentProviderException;
}

// Implementation wraps
public class StripePaymentService implements PaymentService {
    public void charge(Card card) {
        try {
            stripeApi.charge(card);
        } catch (StripeException e) {
            throw new PaymentProviderException("Stripe charge failed", e);
        }
    }
}

You can switch Stripe to PayPal without changing method signatures.

Preventing context loss

// WORSE - loses cause type and stack trace
throw new MyException(e.getMessage());

// BETTER - pass the whole object
throw new MyException("Context: processing order " + orderId, e);

Senior Level

Stack Trace Depth

On deep wrapping (DB -> Hibernate -> Spring -> Service -> Controller) stack trace becomes huge. This increases formatting time and log volume.

Solution: on intermediate layers use “lightweight” exceptions without stack trace, if the original cause (cause) already contains all necessary stack.

Memory Overhead

Each object in the cause chain - a live reference. If you store such objects in memory (error processing queue) - delayed memory cleanup.

Circular References

Throwable is protected from infinite cycles:

e1.initCause(e2);
e2.initCause(e1); // IllegalArgumentException

Reflection and InvocationTargetException

With Method.invoke() any exception is always wrapped in InvocationTargetException:

try {
    method.invoke(obj);
} catch (InvocationTargetException e) {
    Throwable realException = e.getCause(); // Real error
}

Unwrapping for debugging

// 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();
}

Custom Metadata when wrapping

Add metadata known at the current layer:

public class ServiceException extends RuntimeException {
    private final Long orderId;
    private final String action;

    public ServiceException(String message, Throwable cause, Long orderId, String action) {
        super(message, cause);
        this.orderId = orderId;
        this.action = action;
    }
}

Diagnostics

  • Log Analysis - make sure log format reveals cause (%ex in Logback)
  • IntelliJ Debugger - expand cause node to see the entire error tree
  • JSON structured logs - cause as a separate field for indexing

Interview Cheat Sheet

Must know:

  • Wrapping - creating a new exception with the original as cause (matryoshka)
  • Constructor super(message, cause) calls initCause() - can only be called once
  • Chain of causality visible in logs through Caused by:
  • Wrapping changes abstraction level: DAO -> Service -> Controller
  • Context loss: new MyException(e.getMessage()) - bad, loses type and stack trace
  • InvocationTargetException on reflection always wraps the real error
  • Avoid wrap-a-wrap anti-pattern - don’t wrap wrappers of the same level

Frequent follow-up questions:

  • Can you call initCause() twice? - No, will get IllegalStateException
  • Why pass cause instead of just e.getMessage()? - To preserve the type and stack trace of the original error
  • When NOT to wrap? - Validation, programmer errors (NPE), deep nesting 5+ levels
  • How to get root cause manually? - Loop while (t.getCause() != null) t = t.getCause()

Red flags (NOT to say):

  • “I wrap all exceptions indiscriminately” - this is an anti-pattern
  • “I use e.getMessage() instead of cause” - loses type and stack trace
  • “Wrapping is to hide error from caller” - no, to add context
  • “Nesting depth doesn’t matter” - 10+ levels make stack trace unreadable

Related topics:

  • [[28. What is exception chaining]] - same mechanism, different angle
  • [[6. What is Throwable]] - field cause and initCause()
  • [[16. What does the printStackTrace() method do]] - how to see chain in logs
  • [[15. What is a stack trace]] - reading stack trace with Caused by:
  • [[17. How to properly log exceptions]] - logging with %ex