Definition
throw erethrows the original exception unchanged: same type, message, and stack trace. Use when the current layer has nothing to add.throw new ServiceException("msg", e)wraps the original in a higher-level domain exception. Always passeas cause to preserve the chain.- Java 7 precise rethrow: if you
catch (Exception e)but the try block only throwsIOExceptionandSQLException, the compiler allowsthrows IOException, SQLExceptioninstead ofthrows Exception, because it analyzes the actual types that can reach the catch block. This requireseto be effectively final (not reassigned).
Rethrow is the teller receiving a complaint from the vault ("safe jammed"), stamping "seen by teller," and passing the same form to the manager. Wrap is the teller writing a new form ("settlement delayed, cause: safe jammed") and stapling the original inside. The manager sees the business impact first, then reads the technical cause.
Understanding the Concepts
What is rethrowing (throw e)?
Rethrowing passes the original exception unchanged to the caller. Same type, same message, same stack trace.
package com.bank.demo;
public class RethrowDemo {
public static void settle(String tradeId) {
try {
if (tradeId.equals("BAD")) {
throw new IllegalStateException("Settlement engine offline");
}
System.out.println("Settled: " + tradeId);
} catch (IllegalStateException e) {
System.out.println("Logged: " + e.getMessage());
throw e; // Rethrow: same type, message, trace
}
}
public static void main(String[] args) {
try {
settle("BAD");
} catch (IllegalStateException e) {
System.out.println("Caller sees original: " + e.getClass().getSimpleName());
System.out.println("Message: " + e.getMessage());
}
}
}
Logged: Settlement engine offline
Caller sees original: IllegalStateException
Message: Settlement engine offline
What is wrapping (throw new ServiceException(msg, e))?
Wrapping translates a low-level exception into a domain-meaningful one while preserving the original as the cause.
package com.bank.demo;
public class WrapDemo {
static class SettlementException extends RuntimeException {
SettlementException(String msg, Throwable cause) { super(msg, cause); }
}
public static void settle(String tradeId) {
try {
throw new RuntimeException("SQL: constraint violation on trades table");
} catch (RuntimeException e) {
throw new SettlementException(
"Settlement failed for " + tradeId, e);
}
}
public static void main(String[] args) {
try {
settle("TRD-001");
} catch (SettlementException e) {
System.out.println("Domain: " + e.getMessage());
System.out.println("Root cause: " + e.getCause().getMessage());
}
}
}
Domain: Settlement failed for TRD-001
Root cause: SQL: constraint violation on trades table
When should you rethrow vs wrap?
| Scenario | Use | Why |
|---|---|---|
| Same abstraction level | throw e (rethrow) | Nothing to add |
| Crossing abstraction boundary | throw new X(msg, e) (wrap) | Translate to domain language |
| Logging then forwarding | throw e (rethrow) | Log + forward unchanged |
| Repository to Service | wrap | Service should not expose SQL |
| Service to Service | rethrow | Same abstraction |
What is Java 7 precise rethrow?
Java 7 lets the compiler narrow the type when rethrowing from a catch(Exception e) block, as long as e is effectively final.
package com.bank.demo;
import java.io.IOException;
import java.sql.SQLException;
public class PreciseRethrowDemo {
// Java 7+: compiler narrows to actual types
public static void process() throws IOException, SQLException {
try {
readFile(); // throws IOException
queryDb(); // throws SQLException
} catch (Exception e) {
System.out.println("Logging: " + e.getMessage());
throw e; // Compiler knows: only IOException or SQLException
// e is effectively final (not reassigned)
}
}
// If e is reassigned, precise rethrow breaks:
// catch (Exception e) {
// e = new RuntimeException("replaced");
// throw e; // Now must declare throws Exception
// }
private static void readFile() throws IOException { throw new IOException("File error"); }
private static void queryDb() throws SQLException { throw new SQLException("DB error"); }
public static void main(String[] args) {
try {
process();
} catch (IOException | SQLException e) {
System.out.println("Caught: " + e.getMessage());
}
}
}
Logging: File error
Caught: File error
What should a senior engineer know?
- throw e resets the stack trace. If you want to reset the stack trace to the rethrow point, call
throw e.fillInStackTrace(). But this is rarely needed. - Wrapping without cause is a bug.
throw new ServiceException("msg")without the cause is a code review failure. The root cause is permanently lost. - Spring's automatic wrapping. Spring's
@Repositorytranslates vendor-specific SQL exceptions to DataAccessException with the original as the cause. You do not need to wrap manually.
What are common interview follow-up questions?
- Does throw e change the stack trace? (No, it preserves the original.)
- What is the difference between throw e and throw new RuntimeException(e)? (throw e rethrows unchanged, throw new RE(e) wraps.)
- When does precise rethrow not work? (When e is reassigned in the catch block.)
- Should you always wrap at service boundaries? (Yes, to avoid leaking implementation details.)
Visual Overview
throw e (rethrow): throw new X(msg, e) (wrap): ┌──────────────────┐ ┌──────────────────────────┐ │ Same exception │ │ New domain exception │ │ Same type │ │ Different type │ │ Same message │ │ Business message │ │ Same stack trace │ │ Cause = original exception │ │ Nothing added │ │ Context added │ └──────────────────┘ └──────────────────────────┘ Decision: Same layer → rethrow Cross boundary → wrap with cause ALWAYS pass the cause.
Key Takeaway
throw e rethrows the original exception unchanged. Use it within the same abstraction level when you just need to log and forward. throw new ServiceException("msg", e) wraps the original in a domain exception, translating technical errors to business-meaningful ones. Use wrapping at abstraction boundaries: repository to service, service to controller.
The critical rule: always pass the cause when wrapping. throw new SettlementException("msg") without the cause permanently loses the root cause. throw new SettlementException("msg", e) preserves the full "Caused by:" chain.
Java 7 precise rethrow is a compiler feature: if you catch(Exception e) and rethrow without reassigning e, the compiler narrows the declared throws to the actual types thrown in the try block. This lets you use throws IOException, SQLException instead of the broad throws Exception.
Remember one sentence: rethrow within the same layer, wrap at abstraction boundaries, and always pass the original exception as the cause.
---