9.5. Custom Exceptions
Custom exceptions are user-defined exception classes that extend Java's exception hierarchy. They provide meaningful, domain-specific error handling for your application.
Why Create Custom Exceptions?
- Domain-specific errors: Represent business logic violations
- Better error categorization: Group related errors
- Additional context: Include extra information about the error
- Consistent error handling: Standardize across application
Creating Checked Custom Exceptions
Checked exceptions extend Exception and must be declared or handled.
java
// Custom checked exception
public class DatabaseConnectionException extends Exception {
private String databaseUrl;
private int timeout;
public DatabaseConnectionException(String message) {
super(message);
}
public DatabaseConnectionException(String message, String databaseUrl, int timeout) {
super(message);
this.databaseUrl = databaseUrl;
this.timeout = timeout;
}
public DatabaseConnectionException(String message, Throwable cause) {
super(message, cause);
}
// Getters for additional context
public String getDatabaseUrl() {
return databaseUrl;
}
public int getTimeout() {
return timeout;
}
@Override
public String getMessage() {
return super.getMessage() +
" [URL: " + databaseUrl + ", Timeout: " + timeout + "ms]";
}
}Creating Unchecked Custom Exceptions
Unchecked exceptions extend RuntimeException and don't require declaration.
java
// Custom unchecked exception for business logic
public class InsufficientFundsException extends RuntimeException {
private double currentBalance;
private double requiredAmount;
private String accountNumber;
public InsufficientFundsException(double currentBalance, double requiredAmount, String accountNumber) {
super(String.format(
"Insufficient funds in account %s. Current: $%.2f, Required: $%.2f",
accountNumber, currentBalance, requiredAmount));
this.currentBalance = currentBalance;
this.requiredAmount = requiredAmount;
this.accountNumber = accountNumber;
}
// Getters
public double getCurrentBalance() { return currentBalance; }
public double getRequiredAmount() { return requiredAmount; }
public String getAccountNumber() { return accountNumber; }
}Complete Banking System Example
java
// Custom exceptions for banking domain
class InvalidAccountException extends Exception {
public InvalidAccountException(String accountNumber) {
super("Invalid account number: " + accountNumber);
}
}
class TransactionLimitExceededException extends RuntimeException {
private double transactionAmount;
private double maxLimit;
public TransactionLimitExceededException(double amount, double maxLimit) {
super(String.format("Transaction limit exceeded: $%.2f > $%.2f", amount, maxLimit));
this.transactionAmount = amount;
this.maxLimit = maxLimit;
}
public double getTransactionAmount() { return transactionAmount; }
public double getMaxLimit() { return maxLimit; }
}
// Banking service using custom exceptions
public class BankingService {
private static final double MAX_TRANSACTION_AMOUNT = 10000.0;
public void transferMoney(String fromAccount, String toAccount, double amount)
throws InvalidAccountException {
// Validate accounts
if (!isValidAccount(fromAccount)) {
throw new InvalidAccountException(fromAccount);
}
if (!isValidAccount(toAccount)) {
throw new InvalidAccountException(toAccount);
}
// Check transaction limits
if (amount > MAX_TRANSACTION_AMOUNT) {
throw new TransactionLimitExceededException(amount, MAX_TRANSACTION_AMOUNT);
}
// Check funds (unchecked exception)
double balance = getAccountBalance(fromAccount);
if (amount > balance) {
throw new InsufficientFundsException(balance, amount, fromAccount);
}
// Perform transfer
System.out.printf("Transferred $%.2f from %s to %s%n", amount, fromAccount, toAccount);
}
private boolean isValidAccount(String accountNumber) {
return accountNumber != null && accountNumber.matches("\\d{8}");
}
private double getAccountBalance(String accountNumber) {
// Mock implementation
return 5000.0;
}
}Example: E-commerce Application
java
// Custom exceptions for e-commerce
class ProductNotFoundException extends Exception {
private String productId;
public ProductNotFoundException(String productId) {
super("Product not found: " + productId);
this.productId = productId;
}
public String getProductId() { return productId; }
}
class OutOfStockException extends RuntimeException {
private String productId;
private int requestedQuantity;
private int availableQuantity;
public OutOfStockException(String productId, int requested, int available) {
super(String.format(
"Product %s out of stock. Requested: %d, Available: %d",
productId, requested, available));
this.productId = productId;
this.requestedQuantity = requested;
this.availableQuantity = available;
}
// Getters
public String getProductId() { return productId; }
public int getRequestedQuantity() { return requestedQuantity; }
public int getAvailableQuantity() { return availableQuantity; }
}
class InvalidPriceException extends IllegalArgumentException {
public InvalidPriceException(double price) {
super("Invalid price: " + price + ". Price must be positive.");
}
}
// E-commerce service
public class ShoppingCart {
public void addToCart(String productId, int quantity) throws ProductNotFoundException {
// Validate product exists
if (!productExists(productId)) {
throw new ProductNotFoundException(productId);
}
// Check stock
int availableStock = getAvailableStock(productId);
if (quantity > availableStock) {
throw new OutOfStockException(productId, quantity, availableStock);
}
// Add to cart
System.out.println("Added " + quantity + " of product " + productId + " to cart");
}
public void setProductPrice(String productId, double price) {
if (price <= 0) {
throw new InvalidPriceException(price);
}
// Set price
System.out.println("Price set to $" + price + " for product " + productId);
}
private boolean productExists(String productId) {
// Mock implementation
return productId.startsWith("PROD");
}
private int getAvailableStock(String productId) {
// Mock implementation
return 10;
}
}Best Practices for Custom Exceptions
- Meaningful names: Use names that describe the exceptional condition
- Provide constructors: Include common Exception constructors
- Add relevant context: Include fields that help diagnose the problem
- Override getMessage(): Provide detailed, contextual error messages
- Use checked vs unchecked appropriately:
- Checked: When caller should recover from the exception
- Unchecked: For programming errors or unrecoverable conditions
java
// Good custom exception design
public class PaymentProcessingException extends Exception {
private String transactionId;
private BigDecimal amount;
private String paymentMethod;
// Standard exception constructors
public PaymentProcessingException(String message) { super(message); }
public PaymentProcessingException(String message, Throwable cause) { super(message, cause); }
// Domain-specific constructor
public PaymentProcessingException(String message, String transactionId,
BigDecimal amount, String paymentMethod) {
super(message);
this.transactionId = transactionId;
this.amount = amount;
this.paymentMethod = paymentMethod;
}
@Override
public String getMessage() {
return String.format("%s [Transaction: %s, Amount: %s, Method: %s]",
super.getMessage(), transactionId, amount, paymentMethod);
}
// Getters
public String getTransactionId() { return transactionId; }
public BigDecimal getAmount() { return amount; }
public String getPaymentMethod() { return paymentMethod; }
}Summary
Custom exceptions make your code more:
- Readable: Domain-specific exception names
- Maintainable: Consistent error handling
- Debuggable: Rich contextual information
- Robust: Proper error recovery strategies
They bridge the gap between Java's standard exceptions and your application's specific error conditions.
