Definition
Custom exception best practices:
- Extend the right parent:
Exceptionfor recoverable problems (checked).RuntimeExceptionfor programming errors (unchecked). - Descriptive name:
InsufficientFundsException, notMyException. - Include context: Account ID, attempted amount, current balance.
- Preserve the cause: Pass the original exception to
super(message, cause).
A generic bank error slip just says "error." A custom exception slip says: "InsufficientFundsException: Account SAV2001 attempted to debit 100,000 but balance is only 50,000." The second one tells you exactly what went wrong.
Understanding the Concepts
How do you create a custom checked exception?
public class InsufficientFundsException extends Exception {
private final String accountId;
private final BigDecimal attempted;
private final BigDecimal available;
public InsufficientFundsException(String accountId, BigDecimal attempted, BigDecimal available) {
super(String.format("Account %s: attempted %s but only %s available",
accountId, attempted, available));
this.accountId = accountId;
this.attempted = attempted;
this.available = available;
}
public String getAccountId() { return accountId; }
public BigDecimal getAttempted() { return attempted; }
public BigDecimal getAvailable() { return available; }
}
How do you create a custom unchecked exception?
public class AccountNotFoundException extends RuntimeException {
private final String accountId;
public AccountNotFoundException(String accountId) {
super("Account not found: " + accountId);
this.accountId = accountId;
}
public AccountNotFoundException(String accountId, Throwable cause) {
super("Account not found: " + accountId, cause); // preserve the cause
this.accountId = accountId;
}
public String getAccountId() { return accountId; }
}
How do you use them?
public class TransferService {
public void transfer(String fromId, String toId, BigDecimal amount)
throws InsufficientFundsException { // checked: caller must handle
Account from = repository.findById(fromId)
.orElseThrow(() -> new AccountNotFoundException(fromId)); // unchecked: no throws needed
if (from.getBalance().compareTo(amount) < 0) {
throw new InsufficientFundsException(fromId, amount, from.getBalance());
}
// proceed with transfer
}
}
When should you use checked vs unchecked custom exceptions?
- **Checked:** Caller can meaningfully recover. `InsufficientFundsException` → show user a message, suggest a smaller amount. - **Unchecked:** Programming error or unrecoverable situation. `AccountNotFoundException` → should never happen in normal flow, indicates a bug or data inconsistency.
What about Spring @ResponseStatus?
@ResponseStatus(HttpStatus.NOT_FOUND)
public class AccountNotFoundException extends RuntimeException {
// Spring automatically returns 404 when this is thrown from a controller
}
@ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY)
public class InsufficientFundsException extends RuntimeException {
// Spring returns 422
}
What are common mistakes?
- Not including the cause.
throw new CustomException("fail")loses the stack trace of the original error. Usethrow new CustomException("fail", originalException). - Using generic names.
MyException,AppExceptiontell nothing. Name it after the business problem. - Making every custom exception checked. Most modern frameworks (Spring, Jakarta EE) prefer unchecked exceptions.
What should a senior engineer know?
- Exception hierarchy for a project: Create a base
BankException extends RuntimeException, thenInsufficientFundsException extends BankException. CatchBankExceptionat the boundary. - @ControllerAdvice + @ExceptionHandler in Spring provides centralized exception-to-HTTP mapping.
- serialVersionUID: If your exceptions are serialized (remote calls), add
private static final long serialVersionUID = 1L;.
Visual Overview
Custom exception hierarchy for banking:
RuntimeException
└── BankException (base for all bank exceptions)
├── AccountNotFoundException
├── InsufficientFundsException
├── DuplicateTransactionException
└── InvalidAmountException
Each includes: message, business data (accountId, amount), cause
Spring: @ResponseStatus maps to HTTP codes
Key Takeaway
Create custom exceptions by extending Exception (checked, caller must handle) or RuntimeException (unchecked, caller is not forced). Name them after the business problem, include relevant context (account ID, amounts), and always preserve the original cause.
In banking, InsufficientFundsException tells you which account, what amount was attempted, and what was available. AccountNotFoundException tells you which account ID was not found. These are far more useful than generic Exception or a string error code.
One sentence to remember: extend Exception for recoverable problems, RuntimeException for bugs, include the message, the cause, and the business data.
---