What is exception wrapping (wrapping)?
Chain of causality in the log will look like:
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
- Add context:
"Failed to query users table for active users"is clearer than"Connection timeout" - Hide implementation details: business logic shouldn’t know about
SQLException - Preserve cause:
eis passed - original error’s stack trace is not lost - Unified hierarchy: all service exceptions - subtypes of
ServiceException
When NOT to use wrapping
- Input validation -
IllegalArgumentExceptiondoesn’t need wrapping. This is a programming error, it needs to be fixed, not wrapped - Programmer errors -
NullPointerException,IndexOutOfBoundsExceptionshould not be caught and wrapped. These exceptions indicate a bug - let them reach the global handler - Too deep nesting - if already 5+ levels of
cause, don’t add another without good reason. Chain of 10 exceptions = unreadable stack trace - Timeouts and retry - on automatic retry, don’t wrap each attempt, wrap only the final failure. Otherwise you get:
RetryException->TimeoutException->TimeoutException->TimeoutException->SocketException - Performance-critical code - each wrapped exception = new object +
fillInStackTrace(). In hot-path this is noticeable overhead - “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(%exin Logback) - IntelliJ Debugger - expand
causenode to see the entire error tree - JSON structured logs -
causeas 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)callsinitCause()- 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 InvocationTargetExceptionon 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 getIllegalStateException - Why pass
causeinstead of juste.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 ofcause” - 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
causeandinitCause() - [[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