Definition
- Checked custom exception (extends
Exception): use when the caller CAN and SHOULD take a specific recovery action. Example:InsufficientFundsExceptionlets the caller suggest a deposit or smaller amount. - Unchecked custom exception (extends
RuntimeException): use when the failure is a programming error or the caller cannot meaningfully recover. Example:InvalidTradeStateExceptionmeans a code bug. - Always provide constructors accepting
(String message, Throwable cause)and callsuper(message, cause)to preserve the full "Caused by:" chain. - Swallowing exceptions (
catch (Exception e) { }) is the worst anti-pattern: the error disappears silently with zero evidence.
A checked exception is the bank telling Priya: "Your balance is too low. Would you like to deposit more?" She can act on it. An unchecked exception is the system detecting corrupted trade records. Nothing Priya can do about it. Swallowing an exception is the teller seeing an error, crumpling the paper, and telling Priya "everything is fine" while Rs.50,000 vanishes.
Understanding the Concepts
How do you create a custom checked exception?
Extend Exception to create a checked exception. The compiler forces every caller to handle it.
package com.bank.exception;
import java.io.Serial;
import java.math.BigDecimal;
public class InsufficientFundsException extends Exception {
@Serial
private static final long serialVersionUID = 1L;
private final BigDecimal balance;
private final BigDecimal requested;
public InsufficientFundsException(BigDecimal balance, BigDecimal requested) {
super("Balance Rs." + balance + " insufficient for Rs." + requested);
this.balance = balance;
this.requested = requested;
}
public InsufficientFundsException(BigDecimal balance, BigDecimal requested, Throwable cause) {
super("Balance Rs." + balance + " insufficient for Rs." + requested, cause);
this.balance = balance;
this.requested = requested;
}
public BigDecimal getBalance() { return balance; }
public BigDecimal getRequested() { return requested; }
public static void main(String[] args) {
try {
throw new InsufficientFundsException(
new BigDecimal("5000.00"), new BigDecimal("25000.00"));
} catch (InsufficientFundsException e) {
System.out.println("Checked: " + e.getMessage());
System.out.println("Balance: Rs." + e.getBalance());
System.out.println("Requested: Rs." + e.getRequested());
}
}
}
Checked: Balance Rs.5000.00 insufficient for Rs.25000.00
Balance: Rs.5000.00
Requested: Rs.25000.00
How do you create a custom unchecked exception?
Extend RuntimeException for unchecked exceptions. The compiler does not force callers to handle them.
package com.bank.exception;
import java.io.Serial;
public class InvalidTradeStateException extends RuntimeException {
@Serial
private static final long serialVersionUID = 1L;
private final String tradeId;
private final String currentState;
private final String expectedState;
public InvalidTradeStateException(String tradeId, String currentState, String expectedState) {
super("Trade " + tradeId + " in state " + currentState + ", expected " + expectedState);
this.tradeId = tradeId;
this.currentState = currentState;
this.expectedState = expectedState;
}
public InvalidTradeStateException(String message, Throwable cause) {
super(message, cause);
this.tradeId = "UNKNOWN";
this.currentState = "UNKNOWN";
this.expectedState = "UNKNOWN";
}
public String getTradeId() { return tradeId; }
public static void main(String[] args) {
try {
throw new InvalidTradeStateException("TRD-001", "SETTLED", "PENDING");
} catch (InvalidTradeStateException e) {
System.out.println("Unchecked: " + e.getMessage());
}
}
}
Unchecked: Trade TRD-001 in state SETTLED, expected PENDING
When should you use checked vs unchecked?
| Use Case | Checked (Exception) | Unchecked (RuntimeException) |
|---|---|---|
| Caller can recover | Yes: retry, fallback, prompt user | No: fix the code |
| Example | InsufficientFundsException | InvalidTradeStateException |
| Compiler enforces | Yes: try-catch or throws | No |
| Modern preference | Rare, truly recoverable only | Most cases |
| Spring convention | Wrapped in unchecked | Direct use |
Why must you always preserve the cause?
Without the cause, the "Caused by:" chain is missing and debugging becomes guesswork.
package com.bank.demo;
public class PreserveCauseDemo {
// WRONG: cause lost
static RuntimeException wrongWrap(Exception original) {
return new RuntimeException("Settlement failed");
// No cause. "Caused by:" section is GONE.
}
// CORRECT: cause preserved
static RuntimeException correctWrap(Exception original) {
return new RuntimeException("Settlement failed", original);
// Cause preserved. Full chain visible.
}
public static void main(String[] args) {
Exception original = new Exception("Deadlock on accounts table");
RuntimeException wrong = wrongWrap(original);
System.out.println("Wrong - cause: " + wrong.getCause());
RuntimeException correct = correctWrap(original);
System.out.println("Correct - cause: " + correct.getCause().getMessage());
}
}
Wrong - cause: null
Correct - cause: Deadlock on accounts table
Why is swallowing exceptions the worst anti-pattern?
An empty catch block makes the exception disappear silently. In a banking context, this means money can be lost with zero evidence.
package com.bank.demo;
import java.math.BigDecimal;
public class SwallowingAntiPatternDemo {
// WORST: empty catch block
public static void worst() {
try {
System.out.println("Debit SAV2001 Rs.50000: SUCCESS");
throw new RuntimeException("Credit CUR3001 FAILED: DB timeout");
} catch (Exception e) {
// NOTHING. Error disappears.
}
System.out.println("Method returns normally. Caller thinks everything worked.");
System.out.println("Rs.50000 debited but never credited. LOST.\n");
}
// BAD: log but do not rethrow
public static void bad() {
try {
throw new RuntimeException("Credit FAILED");
} catch (Exception e) {
System.out.println("Logged: " + e.getMessage());
// Logged but NOT rethrown. Caller thinks credit succeeded.
}
System.out.println("Caller still thinks operation succeeded.\n");
}
// CORRECT: log AND rethrow
public static void correct() {
try {
throw new RuntimeException("Credit FAILED");
} catch (Exception e) {
System.out.println("Logged: " + e.getMessage());
throw new RuntimeException("Transfer failed: credit step", e);
}
}
public static void main(String[] args) {
System.out.println("=== WORST: empty catch ===");
worst();
System.out.println("=== BAD: log only ===");
bad();
System.out.println("=== CORRECT: log + rethrow ===");
try {
correct();
} catch (RuntimeException e) {
System.out.println("Caller correctly sees failure: " + e.getMessage());
}
}
}
=== WORST: empty catch ===
Debit SAV2001 Rs.50000: SUCCESS
Method returns normally. Caller thinks everything worked.
Rs.50000 debited but never credited. LOST.
=== BAD: log only ===
Logged: Credit FAILED
Caller still thinks operation succeeded.
=== CORRECT: log + rethrow ===
Logged: Credit FAILED
Caller correctly sees failure: Transfer failed: credit step
What constructors should a custom exception have?
Follow the standard four-constructor pattern from Throwable for maximum flexibility.
package com.bank.exception;
import java.io.Serial;
public class TradeProcessingException extends RuntimeException {
@Serial
private static final long serialVersionUID = 1L;
public TradeProcessingException() {
super();
}
public TradeProcessingException(String message) {
super(message);
}
public TradeProcessingException(String message, Throwable cause) {
super(message, cause);
}
public TradeProcessingException(Throwable cause) {
super(cause);
}
public static void main(String[] args) {
TradeProcessingException ex = new TradeProcessingException(
"Failed to process TRD-001",
new RuntimeException("Connection refused"));
System.out.println("Message: " + ex.getMessage());
System.out.println("Cause: " + ex.getCause().getMessage());
}
}
Message: Failed to process TRD-001
Cause: Connection refused
What should a senior engineer know?
- Exception hierarchy design. Create a base
BankingServiceExceptionthat all domain exceptions extend. This lets the global handler catch the base type as a fallback. - SonarQube rules.
squid:S2142flags empty catch blocks.squid:S1166flags missing cause in wrapping. Enable these in CI/CD. - Serialization. Always include
serialVersionUID. Without it, deserialization across different JVM versions may fail with InvalidClassException. - Immutability. Make custom exception fields final. Exceptions should be immutable after construction.
What are common interview follow-up questions?
- Why include serialVersionUID? (For serialization compatibility across JVM versions.)
- Should custom exceptions be final? (Not necessarily, but make them if no subclassing is expected.)
- How do you design an exception hierarchy? (Base domain exception, then specific subtypes for each business error.)
- What is the modern preference? (Unchecked for most cases, global @ControllerAdvice handler.)
Visual Overview
Exception Hierarchy Design: ┌────────────────────────────────┐ │ RuntimeException │ │ ┌───────────────────────────┐ │ │ │ BankingServiceException │ │ ← base domain exception │ │ ┌─────────────────────┐ │ │ │ │ │ TradeNotFoundException│ │ │ ← 404 │ │ │ SettlementException │ │ │ ← 500 │ │ │ InsufficientBalance │ │ │ ← 400 │ │ └─────────────────────┘ │ │ │ └───────────────────────────┘ │ └────────────────────────────────┘ Constructors: ┌─────────────────────────────────────┐ │ MyException() │ │ MyException(String message) │ │ MyException(String msg, Throwable) │ ← MUST HAVE │ MyException(Throwable cause) │ └─────────────────────────────────────┘
Key Takeaway
Custom exceptions should be checked (extends Exception) only when the immediate caller can take a specific recovery action, such as suggesting a smaller withdrawal amount for InsufficientFundsException. For most other cases, extend RuntimeException for unchecked exceptions. The modern Java trend strongly favors unchecked exceptions with centralized global handlers.
Always provide constructors that accept a message and a cause, calling super(message, cause). Without the cause, the "Caused by:" chain is missing from the stack trace. Include serialVersionUID for serialization compatibility. Make exception fields final for immutability.
Swallowing exceptions with empty catch blocks (catch (Exception e) { }) is the single worst anti-pattern in Java. At National Bank of India, a swallowed credit exception meant Rs.50,000 was debited from SAV2001 but never credited to CUR3001. No log, no alert, no evidence. Discovered weeks later through Priya's complaint. The only acceptable patterns are: log and rethrow, log and wrap with cause, or handle and return an explicit error response. Never: empty catch or log-only without rethrowing.
Remember one sentence: extend RuntimeException for most custom exceptions, always preserve the cause with super(message, cause), and never swallow an exception with an empty catch block.
---