1. Introduction to Custom Exceptions
What are Custom Exceptions?
Custom exceptions are user-defined exception classes that extend Java's built-in exception classes. They allow you to create application-specific exceptions with meaningful names and additional functionality.
Why Use Custom Exceptions?
- Application-specific error handling
- Better error categorization and organization
- Additional context and information in exceptions
- Improved code readability and maintainability
- Consistent error reporting across application
Exception Hierarchy:
Throwable ├── Error (unchecked) └── Exception ├── RuntimeException (unchecked) └── Other Exceptions (checked)
2. Types of Custom Exceptions
Checked vs Unchecked Exceptions:
- Checked Exceptions: Must be declared or handled (extend
Exception) - Unchecked Exceptions: Don't require declaration (extend
RuntimeException)
3. Complete Code Examples
Example 1: Basic Custom Exception Structure
// Custom checked exception
class InsufficientFundsException extends Exception {
public InsufficientFundsException(String message) {
super(message);
}
public InsufficientFundsException(String message, Throwable cause) {
super(message, cause);
}
}
// Custom unchecked exception
class InvalidAccountException extends RuntimeException {
public InvalidAccountException(String message) {
super(message);
}
public InvalidAccountException(String message, Throwable cause) {
super(message, cause);
}
}
// Custom exception with additional fields
class BankingException extends Exception {
private String accountNumber;
private double currentBalance;
private double requiredAmount;
public BankingException(String message, String accountNumber,
double currentBalance, double requiredAmount) {
super(message);
this.accountNumber = accountNumber;
this.currentBalance = currentBalance;
this.requiredAmount = requiredAmount;
}
public BankingException(String message, String accountNumber,
double currentBalance, double requiredAmount, Throwable cause) {
super(message, cause);
this.accountNumber = accountNumber;
this.currentBalance = currentBalance;
this.requiredAmount = requiredAmount;
}
// Getters
public String getAccountNumber() { return accountNumber; }
public double getCurrentBalance() { return currentBalance; }
public double getRequiredAmount() { return requiredAmount; }
public double getShortfall() { return requiredAmount - currentBalance; }
@Override
public String toString() {
return String.format("%s [Account: %s, Balance: %.2f, Required: %.2f, Shortfall: %.2f]",
super.toString(), accountNumber, currentBalance, requiredAmount, getShortfall());
}
}
public class BasicCustomExceptionExample {
public static void main(String[] args) {
System.out.println("=== Basic Custom Exception Examples ===");
// 1. Using custom checked exception
try {
processTransaction("ACC001", 1000, 1500);
} catch (InsufficientFundsException e) {
System.out.println("Caught InsufficientFundsException: " + e.getMessage());
}
// 2. Using custom unchecked exception
try {
validateAccount(null);
} catch (InvalidAccountException e) {
System.out.println("Caught InvalidAccountException: " + e.getMessage());
}
// 3. Using custom exception with additional fields
try {
processBankingTransaction("ACC002", 500, 800);
} catch (BankingException e) {
System.out.println("Caught BankingException: " + e);
System.out.println("Shortfall amount: $" + e.getShortfall());
}
// 4. Exception with cause
try {
processWithCause();
} catch (BankingException e) {
System.out.println("\nException with cause:");
System.out.println("Message: " + e.getMessage());
System.out.println("Cause: " + e.getCause().getMessage());
e.printStackTrace();
}
}
public static void processTransaction(String accountNumber, double balance, double amount)
throws InsufficientFundsException {
if (amount > balance) {
throw new InsufficientFundsException(
String.format("Insufficient funds in account %s. Balance: $%.2f, Required: $%.2f",
accountNumber, balance, amount));
}
System.out.println("Transaction processed successfully");
}
public static void validateAccount(String accountNumber) {
if (accountNumber == null || accountNumber.trim().isEmpty()) {
throw new InvalidAccountException("Account number cannot be null or empty");
}
System.out.println("Account validated: " + accountNumber);
}
public static void processBankingTransaction(String accountNumber, double balance, double amount)
throws BankingException {
if (amount > balance) {
throw new BankingException("Transaction failed due to insufficient funds",
accountNumber, balance, amount);
}
System.out.println("Banking transaction processed successfully");
}
public static void processWithCause() throws BankingException {
try {
// Simulate an IO operation that might fail
throw new java.io.IOException("Database connection failed");
} catch (java.io.IOException e) {
throw new BankingException("Failed to process banking operation",
"ACC003", 1000, 500, e);
}
}
}
Example 2: Real-World Banking System
import java.util.*;
// Custom exceptions for banking system
class BankAccountNotFoundException extends Exception {
private String accountNumber;
public BankAccountNotFoundException(String accountNumber) {
super("Bank account not found: " + accountNumber);
this.accountNumber = accountNumber;
}
public BankAccountNotFoundException(String accountNumber, Throwable cause) {
super("Bank account not found: " + accountNumber, cause);
this.accountNumber = accountNumber;
}
public String getAccountNumber() { return accountNumber; }
}
class InvalidAmountException extends IllegalArgumentException {
private double amount;
public InvalidAmountException(double amount) {
super("Invalid amount: " + amount + ". Amount must be positive.");
this.amount = amount;
}
public double getAmount() { return amount; }
}
class TransactionLimitExceededException extends RuntimeException {
private double transactionAmount;
private double limit;
public TransactionLimitExceededException(double transactionAmount, double limit) {
super(String.format("Transaction limit exceeded. Amount: %.2f, Limit: %.2f",
transactionAmount, limit));
this.transactionAmount = transactionAmount;
this.limit = limit;
}
public double getTransactionAmount() { return transactionAmount; }
public double getLimit() { return limit; }
}
class AccountBlockedException extends Exception {
private String accountNumber;
private String reason;
public AccountBlockedException(String accountNumber, String reason) {
super("Account " + accountNumber + " is blocked. Reason: " + reason);
this.accountNumber = accountNumber;
this.reason = reason;
}
public String getAccountNumber() { return accountNumber; }
public String getReason() { return reason; }
}
// Bank Account class
class BankAccount {
private String accountNumber;
private String accountHolder;
private double balance;
private boolean isActive;
private List<String> transactionHistory;
private static final double DAILY_LIMIT = 10000.0;
public BankAccount(String accountNumber, String accountHolder, double initialBalance) {
this.accountNumber = accountNumber;
this.accountHolder = accountHolder;
this.balance = initialBalance;
this.isActive = true;
this.transactionHistory = new ArrayList<>();
this.transactionHistory.add("Account created with initial balance: $" + initialBalance);
}
public void deposit(double amount) throws InvalidAmountException, AccountBlockedException {
validateAccountStatus();
if (amount <= 0) {
throw new InvalidAmountException(amount);
}
balance += amount;
transactionHistory.add("Deposited: $" + amount);
System.out.println("Successfully deposited $" + amount);
}
public void withdraw(double amount) throws InsufficientFundsException,
InvalidAmountException,
TransactionLimitExceededException,
AccountBlockedException {
validateAccountStatus();
if (amount <= 0) {
throw new InvalidAmountException(amount);
}
if (amount > DAILY_LIMIT) {
throw new TransactionLimitExceededException(amount, DAILY_LIMIT);
}
if (amount > balance) {
throw new InsufficientFundsException(
String.format("Cannot withdraw $%.2f. Available balance: $%.2f", amount, balance));
}
balance -= amount;
transactionHistory.add("Withdrawn: $" + amount);
System.out.println("Successfully withdrawn $" + amount);
}
public void transfer(BankAccount recipient, double amount)
throws InsufficientFundsException,
InvalidAmountException,
TransactionLimitExceededException,
AccountBlockedException {
validateAccountStatus();
recipient.validateAccountStatus();
if (amount <= 0) {
throw new InvalidAmountException(amount);
}
if (amount > DAILY_LIMIT) {
throw new TransactionLimitExceededException(amount, DAILY_LIMIT);
}
if (amount > balance) {
throw new InsufficientFundsException(
String.format("Cannot transfer $%.2f. Available balance: $%.2f", amount, balance));
}
this.balance -= amount;
recipient.balance += amount;
this.transactionHistory.add("Transferred $" + amount + " to " + recipient.accountNumber);
recipient.transactionHistory.add("Received $" + amount + " from " + this.accountNumber);
System.out.println("Successfully transferred $" + amount + " to " + recipient.accountHolder);
}
private void validateAccountStatus() throws AccountBlockedException {
if (!isActive) {
throw new AccountBlockedException(accountNumber, "Account suspended by administrator");
}
}
public void blockAccount(String reason) {
this.isActive = false;
transactionHistory.add("Account blocked. Reason: " + reason);
}
public void activateAccount() {
this.isActive = true;
transactionHistory.add("Account activated");
}
// Getters
public String getAccountNumber() { return accountNumber; }
public String getAccountHolder() { return accountHolder; }
public double getBalance() { return balance; }
public boolean isActive() { return isActive; }
public List<String> getTransactionHistory() { return new ArrayList<>(transactionHistory); }
@Override
public String toString() {
return String.format("BankAccount{accountNumber='%s', holder='%s', balance=%.2f, active=%s}",
accountNumber, accountHolder, balance, isActive);
}
}
// Bank Service class
class BankService {
private Map<String, BankAccount> accounts;
public BankService() {
this.accounts = new HashMap<>();
}
public void addAccount(BankAccount account) {
accounts.put(account.getAccountNumber(), account);
}
public BankAccount getAccount(String accountNumber) throws BankAccountNotFoundException {
BankAccount account = accounts.get(accountNumber);
if (account == null) {
throw new BankAccountNotFoundException(accountNumber);
}
return account;
}
public void processDeposit(String accountNumber, double amount) {
try {
BankAccount account = getAccount(accountNumber);
account.deposit(amount);
} catch (BankAccountNotFoundException | InvalidAmountException | AccountBlockedException e) {
System.err.println("Deposit failed: " + e.getMessage());
// Log the exception or handle it appropriately
}
}
public void processWithdrawal(String accountNumber, double amount) {
try {
BankAccount account = getAccount(accountNumber);
account.withdraw(amount);
} catch (BankAccountNotFoundException | InsufficientFundsException |
InvalidAmountException | TransactionLimitExceededException |
AccountBlockedException e) {
System.err.println("Withdrawal failed: " + e.getMessage());
}
}
public void processTransfer(String fromAccount, String toAccount, double amount) {
try {
BankAccount source = getAccount(fromAccount);
BankAccount destination = getAccount(toAccount);
source.transfer(destination, amount);
} catch (BankAccountNotFoundException | InsufficientFundsException |
InvalidAmountException | TransactionLimitExceededException |
AccountBlockedException e) {
System.err.println("Transfer failed: " + e.getMessage());
}
}
public void printAccountStatement(String accountNumber) {
try {
BankAccount account = getAccount(accountNumber);
System.out.println("\n=== Account Statement for " + accountNumber + " ===");
System.out.println("Holder: " + account.getAccountHolder());
System.out.println("Balance: $" + account.getBalance());
System.out.println("Status: " + (account.isActive() ? "Active" : "Blocked"));
System.out.println("\nTransaction History:");
account.getTransactionHistory().forEach(tx -> System.out.println(" " + tx));
} catch (BankAccountNotFoundException e) {
System.err.println("Cannot generate statement: " + e.getMessage());
}
}
}
public class BankingSystemExample {
public static void main(String[] args) {
System.out.println("=== Banking System with Custom Exceptions ===");
BankService bankService = new BankService();
// Create accounts
BankAccount aliceAccount = new BankAccount("ACC001", "Alice Johnson", 5000);
BankAccount bobAccount = new BankAccount("ACC002", "Bob Smith", 3000);
bankService.addAccount(aliceAccount);
bankService.addAccount(bobAccount);
// Test various operations
System.out.println("\n1. Testing valid operations:");
bankService.processDeposit("ACC001", 1000);
bankService.processWithdrawal("ACC002", 500);
bankService.processTransfer("ACC001", "ACC002", 300);
System.out.println("\n2. Testing exception scenarios:");
// Invalid amount
bankService.processDeposit("ACC001", -100);
// Insufficient funds
bankService.processWithdrawal("ACC002", 5000);
// Transaction limit exceeded
bankService.processWithdrawal("ACC001", 15000);
// Non-existent account
bankService.processDeposit("ACC999", 1000);
// Blocked account
System.out.println("\n3. Testing blocked account:");
aliceAccount.blockAccount("Suspicious activity");
bankService.processWithdrawal("ACC001", 100);
aliceAccount.activateAccount(); // Reactivate
System.out.println("\n4. Account statements:");
bankService.printAccountStatement("ACC001");
bankService.printAccountStatement("ACC002");
bankService.printAccountStatement("ACC999"); // Non-existent
// Demonstrate exception handling with detailed information
System.out.println("\n5. Detailed exception handling:");
try {
BankAccount account = bankService.getAccount("ACC001");
account.withdraw(10000); // This should exceed daily limit
} catch (TransactionLimitExceededException e) {
System.err.println("Transaction limit error:");
System.err.println(" Attempted: $" + e.getTransactionAmount());
System.err.println(" Limit: $" + e.getLimit());
System.err.println(" Over by: $" + (e.getTransactionAmount() - e.getLimit()));
} catch (Exception e) {
System.err.println("Unexpected error: " + e.getMessage());
}
}
}
Example 3: E-Commerce System with Custom Exceptions
import java.util.*;
import java.time.LocalDateTime;
// Custom exceptions for e-commerce system
class ProductNotFoundException extends Exception {
private String productId;
public ProductNotFoundException(String productId) {
super("Product not found: " + productId);
this.productId = productId;
}
public ProductNotFoundException(String productId, Throwable cause) {
super("Product not found: " + productId, cause);
this.productId = productId;
}
public String getProductId() { return productId; }
}
class InsufficientStockException extends Exception {
private String productId;
private int requestedQuantity;
private int availableQuantity;
public InsufficientStockException(String productId, int requestedQuantity, int availableQuantity) {
super(String.format("Insufficient stock for product %s. Requested: %d, Available: %d",
productId, requestedQuantity, availableQuantity));
this.productId = productId;
this.requestedQuantity = requestedQuantity;
this.availableQuantity = availableQuantity;
}
public String getProductId() { return productId; }
public int getRequestedQuantity() { return requestedQuantity; }
public int getAvailableQuantity() { return availableQuantity; }
public int getShortage() { return requestedQuantity - availableQuantity; }
}
class InvalidPriceException extends IllegalArgumentException {
private double price;
public InvalidPriceException(double price) {
super("Invalid price: " + price + ". Price must be positive.");
this.price = price;
}
public double getPrice() { return price; }
}
class OrderProcessingException extends Exception {
private String orderId;
private LocalDateTime timestamp;
public OrderProcessingException(String orderId, String message) {
super(message);
this.orderId = orderId;
this.timestamp = LocalDateTime.now();
}
public OrderProcessingException(String orderId, String message, Throwable cause) {
super(message, cause);
this.orderId = orderId;
this.timestamp = LocalDateTime.now();
}
public String getOrderId() { return orderId; }
public LocalDateTime getTimestamp() { return timestamp; }
@Override
public String toString() {
return String.format("OrderProcessingException[orderId=%s, time=%s, message=%s]",
orderId, timestamp, getMessage());
}
}
class PaymentFailedException extends Exception {
private String transactionId;
private double amount;
private String failureReason;
public PaymentFailedException(String transactionId, double amount, String failureReason) {
super(String.format("Payment failed for transaction %s. Amount: %.2f, Reason: %s",
transactionId, amount, failureReason));
this.transactionId = transactionId;
this.amount = amount;
this.failureReason = failureReason;
}
public String getTransactionId() { return transactionId; }
public double getAmount() { return amount; }
public String getFailureReason() { return failureReason; }
}
// Product class
class Product {
private String id;
private String name;
private String description;
private double price;
private int stockQuantity;
private String category;
public Product(String id, String name, String description, double price,
int stockQuantity, String category) {
if (price <= 0) {
throw new InvalidPriceException(price);
}
this.id = id;
this.name = name;
this.description = description;
this.price = price;
this.stockQuantity = stockQuantity;
this.category = category;
}
public void reduceStock(int quantity) throws InsufficientStockException {
if (quantity > stockQuantity) {
throw new InsufficientStockException(id, quantity, stockQuantity);
}
stockQuantity -= quantity;
}
public void increaseStock(int quantity) {
if (quantity <= 0) {
throw new IllegalArgumentException("Quantity must be positive");
}
stockQuantity += quantity;
}
// Getters
public String getId() { return id; }
public String getName() { return name; }
public String getDescription() { return description; }
public double getPrice() { return price; }
public int getStockQuantity() { return stockQuantity; }
public String getCategory() { return category; }
@Override
public String toString() {
return String.format("Product{id='%s', name='%s', price=%.2f, stock=%d, category='%s'}",
id, name, price, stockQuantity, category);
}
}
// Order class
class Order {
private String orderId;
private String customerId;
private LocalDateTime orderDate;
private OrderStatus status;
private List<OrderItem> items;
private double totalAmount;
public Order(String orderId, String customerId) {
this.orderId = orderId;
this.customerId = customerId;
this.orderDate = LocalDateTime.now();
this.status = OrderStatus.PENDING;
this.items = new ArrayList<>();
this.totalAmount = 0.0;
}
public void addItem(Product product, int quantity) throws InsufficientStockException {
product.reduceStock(quantity);
OrderItem item = new OrderItem(product, quantity);
items.add(item);
totalAmount += product.getPrice() * quantity;
}
public void processPayment() throws PaymentFailedException {
// Simulate payment processing
if (totalAmount > 1000) { // Simulate payment failure for large amounts
throw new PaymentFailedException(generateTransactionId(), totalAmount,
"Amount exceeds limit");
}
this.status = OrderStatus.PAID;
System.out.println("Payment processed successfully for order: " + orderId);
}
public void completeOrder() {
this.status = OrderStatus.COMPLETED;
}
public void cancelOrder() {
this.status = OrderStatus.CANCELLED;
// Restore stock
for (OrderItem item : items) {
item.getProduct().increaseStock(item.getQuantity());
}
}
private String generateTransactionId() {
return "TXN_" + orderId + "_" + System.currentTimeMillis();
}
// Getters
public String getOrderId() { return orderId; }
public String getCustomerId() { return customerId; }
public LocalDateTime getOrderDate() { return orderDate; }
public OrderStatus getStatus() { return status; }
public List<OrderItem> getItems() { return new ArrayList<>(items); }
public double getTotalAmount() { return totalAmount; }
@Override
public String toString() {
return String.format("Order{id='%s', customer='%s', status=%s, total=%.2f, items=%d}",
orderId, customerId, status, totalAmount, items.size());
}
}
// Order Item class
class OrderItem {
private Product product;
private int quantity;
private double itemTotal;
public OrderItem(Product product, int quantity) {
this.product = product;
this.quantity = quantity;
this.itemTotal = product.getPrice() * quantity;
}
// Getters
public Product getProduct() { return product; }
public int getQuantity() { return quantity; }
public double getItemTotal() { return itemTotal; }
@Override
public String toString() {
return String.format("OrderItem{product='%s', quantity=%d, total=%.2f}",
product.getName(), quantity, itemTotal);
}
}
// Order Status enum
enum OrderStatus {
PENDING, PAID, COMPLETED, CANCELLED, FAILED
}
// E-Commerce Service
class ECommerceService {
private Map<String, Product> products;
private Map<String, Order> orders;
private int orderCounter;
public ECommerceService() {
this.products = new HashMap<>();
this.orders = new HashMap<>();
this.orderCounter = 1;
}
public void addProduct(Product product) {
products.put(product.getId(), product);
}
public Product getProduct(String productId) throws ProductNotFoundException {
Product product = products.get(productId);
if (product == null) {
throw new ProductNotFoundException(productId);
}
return product;
}
public Order createOrder(String customerId, Map<String, Integer> cart)
throws OrderProcessingException {
String orderId = "ORD" + (orderCounter++);
Order order = new Order(orderId, customerId);
try {
for (Map.Entry<String, Integer> entry : cart.entrySet()) {
String productId = entry.getKey();
int quantity = entry.getValue();
Product product = getProduct(productId);
order.addItem(product, quantity);
}
orders.put(orderId, order);
return order;
} catch (ProductNotFoundException | InsufficientStockException e) {
// Rollback any stock reductions
order.cancelOrder();
throw new OrderProcessingException(orderId, "Failed to create order", e);
}
}
public void processOrderPayment(String orderId) throws OrderProcessingException {
try {
Order order = orders.get(orderId);
if (order == null) {
throw new OrderProcessingException(orderId, "Order not found");
}
order.processPayment();
order.completeOrder();
System.out.println("Order completed successfully: " + orderId);
} catch (PaymentFailedException e) {
Order order = orders.get(orderId);
if (order != null) {
order.cancelOrder();
}
throw new OrderProcessingException(orderId, "Payment processing failed", e);
}
}
public void displayProducts() {
System.out.println("\n=== Available Products ===");
products.values().forEach(System.out::println);
}
public void displayOrders() {
System.out.println("\n=== All Orders ===");
orders.values().forEach(System.out::println);
}
}
public class ECommerceSystemExample {
public static void main(String[] args) {
System.out.println("=== E-Commerce System with Custom Exceptions ===");
ECommerceService ecommerce = new ECommerceService();
// Add products
ecommerce.addProduct(new Product("P001", "Laptop", "Gaming Laptop", 999.99, 10, "Electronics"));
ecommerce.addProduct(new Product("P002", "Mouse", "Wireless Mouse", 25.50, 50, "Electronics"));
ecommerce.addProduct(new Product("P003", "Keyboard", "Mechanical Keyboard", 79.99, 20, "Electronics"));
ecommerce.addProduct(new Product("P004", "Book", "Java Programming", 49.99, 100, "Books"));
ecommerce.displayProducts();
// Test scenarios
System.out.println("\n1. Testing successful order:");
try {
Map<String, Integer> cart1 = new HashMap<>();
cart1.put("P001", 1);
cart1.put("P002", 2);
Order order1 = ecommerce.createOrder("CUST001", cart1);
System.out.println("Order created: " + order1);
ecommerce.processOrderPayment(order1.getOrderId());
} catch (OrderProcessingException e) {
System.err.println("Order processing failed: " + e.getMessage());
if (e.getCause() != null) {
System.err.println("Root cause: " + e.getCause().getMessage());
}
}
System.out.println("\n2. Testing insufficient stock:");
try {
Map<String, Integer> cart2 = new HashMap<>();
cart2.put("P001", 15); // Only 9 left after previous order
Order order2 = ecommerce.createOrder("CUST002", cart2);
System.out.println("Order created: " + order2);
} catch (OrderProcessingException e) {
System.err.println("Order creation failed: " + e.getMessage());
if (e.getCause() instanceof InsufficientStockException) {
InsufficientStockException ise = (InsufficientStockException) e.getCause();
System.err.println("Stock details - Available: " + ise.getAvailableQuantity() +
", Requested: " + ise.getRequestedQuantity());
}
}
System.out.println("\n3. Testing non-existent product:");
try {
Map<String, Integer> cart3 = new HashMap<>();
cart3.put("P999", 1); // Non-existent product
Order order3 = ecommerce.createOrder("CUST003", cart3);
System.out.println("Order created: " + order3);
} catch (OrderProcessingException e) {
System.err.println("Order creation failed: " + e.getMessage());
}
System.out.println("\n4. Testing payment failure (large amount):");
try {
Map<String, Integer> cart4 = new HashMap<>();
cart4.put("P001", 2); // Total ~$2000, which should fail payment
Order order4 = ecommerce.createOrder("CUST004", cart4);
System.out.println("Order created: " + order4);
ecommerce.processOrderPayment(order4.getOrderId());
} catch (OrderProcessingException e) {
System.err.println("Order processing failed: " + e.getMessage());
if (e.getCause() instanceof PaymentFailedException) {
PaymentFailedException pfe = (PaymentFailedException) e.getCause();
System.err.println("Payment failure details:");
System.err.println(" Transaction: " + pfe.getTransactionId());
System.err.println(" Amount: $" + pfe.getAmount());
System.err.println(" Reason: " + pfe.getFailureReason());
}
}
System.out.println("\n5. Testing invalid price:");
try {
Product invalidProduct = new Product("P005", "Test", "Test", -10.0, 5, "Test");
} catch (InvalidPriceException e) {
System.err.println("Invalid price detected: " + e.getMessage());
System.err.println("Provided price: $" + e.getPrice());
}
ecommerce.displayProducts();
ecommerce.displayOrders();
}
}
Example 4: Advanced Exception Handling Patterns
import java.util.*;
import java.util.function.Supplier;
// Custom exception with error codes
class BusinessException extends Exception {
private final String errorCode;
private final Map<String, Object> context;
public BusinessException(String errorCode, String message) {
super(message);
this.errorCode = errorCode;
this.context = new HashMap<>();
}
public BusinessException(String errorCode, String message, Throwable cause) {
super(message, cause);
this.errorCode = errorCode;
this.context = new HashMap<>();
}
public BusinessException addContext(String key, Object value) {
this.context.put(key, value);
return this;
}
public String getErrorCode() { return errorCode; }
public Map<String, Object> getContext() { return new HashMap<>(context); }
@Override
public String toString() {
return String.format("BusinessException[code=%s, message=%s, context=%s]",
errorCode, getMessage(), context);
}
}
// Exception wrapper utility
class ExceptionHandler {
// Execute with automatic exception wrapping
public static <T> T executeWithHandling(Supplier<T> operation, String errorCode)
throws BusinessException {
try {
return operation.get();
} catch (Exception e) {
throw new BusinessException(errorCode, "Operation failed", e)
.addContext("timestamp", new Date())
.addContext("thread", Thread.currentThread().getName());
}
}
// Execute with retry logic
public static <T> T executeWithRetry(Supplier<T> operation, int maxRetries,
long delayMs, String errorCode) throws BusinessException {
int attempt = 0;
while (attempt < maxRetries) {
try {
return operation.get();
} catch (Exception e) {
attempt++;
if (attempt == maxRetries) {
throw new BusinessException(errorCode,
"Operation failed after " + maxRetries + " attempts", e)
.addContext("attempts", attempt)
.addContext("maxRetries", maxRetries);
}
System.err.println("Attempt " + attempt + " failed, retrying...");
try {
Thread.sleep(delayMs);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new BusinessException(errorCode, "Operation interrupted", ie);
}
}
}
throw new BusinessException(errorCode, "Unexpected state in retry logic");
}
// Execute and convert to BusinessException
public static <T> T executeAndConvert(Supplier<T> operation,
Supplier<BusinessException> exceptionSupplier)
throws BusinessException {
try {
return operation.get();
} catch (Exception e) {
BusinessException be = exceptionSupplier.get();
if (be.getCause() == null) {
be.initCause(e);
}
throw be;
}
}
}
// Validation framework with custom exceptions
class ValidationException extends BusinessException {
private final String fieldName;
private final Object invalidValue;
public ValidationException(String fieldName, Object invalidValue, String message) {
super("VALIDATION_ERROR", message);
this.fieldName = fieldName;
this.invalidValue = invalidValue;
addContext("field", fieldName)
.addContext("invalidValue", invalidValue)
.addContext("validationType", "FIELD_VALIDATION");
}
public String getFieldName() { return fieldName; }
public Object getInvalidValue() { return invalidValue; }
}
class UserRegistrationException extends BusinessException {
public UserRegistrationException(String message, Throwable cause) {
super("USER_REGISTRATION_ERROR", message, cause);
addContext("service", "UserRegistration")
.addContext("timestamp", System.currentTimeMillis());
}
}
// User service with advanced exception handling
class UserService {
private Set<String> registeredUsernames = new HashSet<>();
public void registerUser(String username, String email, int age)
throws UserRegistrationException {
// Use exception handler with automatic wrapping
ExceptionHandler.executeWithHandling(() -> {
validateUsername(username);
validateEmail(email);
validateAge(age);
// Simulate registration
if (registeredUsernames.contains(username)) {
throw new BusinessException("USERNAME_EXISTS",
"Username already exists: " + username)
.addContext("username", username)
.addContext("available", false);
}
registeredUsernames.add(username);
System.out.println("User registered successfully: " + username);
return null; // Supplier requires return value
}, "REGISTRATION_FAILED");
}
private void validateUsername(String username) throws ValidationException {
if (username == null || username.trim().isEmpty()) {
throw new ValidationException("username", username, "Username cannot be empty");
}
if (username.length() < 3) {
throw new ValidationException("username", username,
"Username must be at least 3 characters long");
}
if (!username.matches("[a-zA-Z0-9_]+")) {
throw new ValidationException("username", username,
"Username can only contain letters, numbers, and underscores");
}
}
private void validateEmail(String email) throws ValidationException {
if (email == null || !email.contains("@")) {
throw new ValidationException("email", email, "Invalid email format");
}
}
private void validateAge(int age) throws ValidationException {
if (age < 13) {
throw new ValidationException("age", age, "User must be at least 13 years old");
}
if (age > 120) {
throw new ValidationException("age", age, "Invalid age value");
}
}
// Method with retry logic
public boolean checkUsernameAvailability(String username) throws BusinessException {
return ExceptionHandler.executeWithRetry(() -> {
// Simulate flaky service
if (Math.random() < 0.3) {
throw new RuntimeException("Service temporarily unavailable");
}
return !registeredUsernames.contains(username);
}, 3, 1000, "USERNAME_CHECK_FAILED");
}
}
public class AdvancedExceptionHandling {
public static void main(String[] args) {
System.out.println("=== Advanced Exception Handling Patterns ===");
UserService userService = new UserService();
// Test successful registration
System.out.println("1. Testing successful registration:");
try {
userService.registerUser("john_doe", "[email protected]", 25);
System.out.println("Registration successful!");
} catch (UserRegistrationException e) {
System.err.println("Registration failed: " + e);
e.getContext().forEach((k, v) -> System.err.println(" " + k + ": " + v));
}
// Test validation failures
System.out.println("\n2. Testing validation failures:");
testValidation(userService, "", "[email protected]", 25); // Empty username
testValidation(userService, "ab", "[email protected]", 25); // Short username
testValidation(userService, "john@doe", "[email protected]", 25); // Invalid chars
testValidation(userService, "validuser", "invalid-email", 25); // Invalid email
testValidation(userService, "validuser", "[email protected]", 10); // Underage
// Test duplicate username
System.out.println("\n3. Testing duplicate username:");
try {
userService.registerUser("john_doe", "[email protected]", 30);
} catch (UserRegistrationException e) {
System.err.println("Registration failed: " + e.getMessage());
if (e.getCause() instanceof BusinessException) {
BusinessException be = (BusinessException) e.getCause();
System.err.println("Error code: " + be.getErrorCode());
be.getContext().forEach((k, v) -> System.err.println(" " + k + ": " + v));
}
}
// Test retry logic
System.out.println("\n4. Testing retry logic:");
try {
boolean available = userService.checkUsernameAvailability("new_user");
System.out.println("Username available: " + available);
} catch (BusinessException e) {
System.err.println("Username check failed: " + e);
e.getContext().forEach((k, v) -> System.err.println(" " + k + ": " + v));
}
// Test exception conversion
System.out.println("\n5. Testing exception conversion:");
try {
String result = ExceptionHandler.executeAndConvert(
() -> {
// Simulate operation that might throw different exceptions
if (Math.random() > 0.5) {
throw new IllegalArgumentException("Invalid argument");
} else {
throw new IllegalStateException("Invalid state");
}
},
() -> new BusinessException("CONVERSION_TEST", "Converted exception")
);
} catch (BusinessException e) {
System.err.println("Caught converted exception: " + e.getMessage());
System.err.println("Original cause: " + e.getCause().getClass().getSimpleName() +
": " + e.getCause().getMessage());
}
}
private static void testValidation(UserService userService, String username,
String email, int age) {
try {
userService.registerUser(username, email, age);
} catch (UserRegistrationException e) {
System.err.println("Validation failed for username='" + username + "': " +
e.getCause().getMessage());
if (e.getCause() instanceof ValidationException) {
ValidationException ve = (ValidationException) e.getCause();
System.err.println(" Field: " + ve.getFieldName());
System.err.println(" Invalid value: " + ve.getInvalidValue());
}
}
}
}
Example 5: Best Practices and Common Patterns
import java.util.*;
import java.util.logging.*;
// Custom exception with logging
class LoggableException extends Exception {
private static final Logger logger = Logger.getLogger(LoggableException.class.getName());
private final String component;
private final String operation;
private final Map<String, Object> metrics;
public LoggableException(String component, String operation, String message) {
super(message);
this.component = component;
this.operation = operation;
this.metrics = new HashMap<>();
logException();
}
public LoggableException(String component, String operation, String message, Throwable cause) {
super(message, cause);
this.component = component;
this.operation = operation;
this.metrics = new HashMap<>();
logException();
}
public LoggableException addMetric(String name, Object value) {
metrics.put(name, value);
return this;
}
private void logException() {
String logMessage = String.format("[%s::%s] %s - Metrics: %s",
component, operation, getMessage(), metrics);
if (getCause() != null) {
logger.log(Level.SEVERE, logMessage, this);
} else {
logger.log(Level.WARNING, logMessage);
}
}
public String getComponent() { return component; }
public String getOperation() { return operation; }
public Map<String, Object> getMetrics() { return new HashMap<>(metrics); }
}
// Exception factory pattern
class ExceptionFactory {
public static LoggableException createDatabaseException(String operation,
String query,
Throwable cause) {
return new LoggableException("Database", operation,
"Database operation failed: " + operation, cause)
.addMetric("query", query)
.addMetric("timestamp", System.currentTimeMillis());
}
public static LoggableException createNetworkException(String operation,
String endpoint,
int statusCode) {
return new LoggableException("Network", operation,
String.format("Network request failed. Endpoint: %s, Status: %d",
endpoint, statusCode))
.addMetric("endpoint", endpoint)
.addMetric("statusCode", statusCode)
.addMetric("retryCount", 0);
}
public static LoggableException createValidationException(String operation,
String field,
Object value,
String rule) {
return new LoggableException("Validation", operation,
String.format("Validation failed for field '%s'. Value: %s, Rule: %s",
field, value, rule))
.addMetric("field", field)
.addMetric("value", value)
.addMetric("rule", rule);
}
}
// Resource management with custom exceptions
class ResourceManager {
public static class ResourceNotFoundException extends LoggableException {
private final String resourceId;
private final String resourceType;
public ResourceNotFoundException(String resourceId, String resourceType) {
super("ResourceManager", "getResource",
String.format("Resource not found. Type: %s, ID: %s", resourceType, resourceId));
this.resourceId = resourceId;
this.resourceType = resourceType;
addMetric("resourceId", resourceId)
.addMetric("resourceType", resourceType);
}
public String getResourceId() { return resourceId; }
public String getResourceType() { return resourceType; }
}
public static class ResourceLockException extends LoggableException {
private final String resourceId;
private final String lockOwner;
public ResourceLockException(String resourceId, String lockOwner, String operation) {
super("ResourceManager", operation,
String.format("Resource locked. ID: %s, Owner: %s", resourceId, lockOwner));
this.resourceId = resourceId;
this.lockOwner = lockOwner;
addMetric("resourceId", resourceId)
.addMetric("lockOwner", lockOwner)
.addMetric("operation", operation);
}
public String getResourceId() { return resourceId; }
public String getLockOwner() { return lockOwner; }
}
}
// Service with comprehensive exception handling
class DataService {
private Map<String, String> dataStore = new HashMap<>();
private Set<String> lockedResources = new HashSet<>();
public String getData(String resourceId) throws ResourceManager.ResourceNotFoundException {
if (!dataStore.containsKey(resourceId)) {
throw new ResourceManager.ResourceNotFoundException(resourceId, "Data");
}
return dataStore.get(resourceId);
}
public void updateData(String resourceId, String data, String lockOwner)
throws ResourceManager.ResourceNotFoundException, ResourceManager.ResourceLockException {
// Check if resource exists
if (!dataStore.containsKey(resourceId)) {
throw new ResourceManager.ResourceNotFoundException(resourceId, "Data");
}
// Check if resource is locked
if (lockedResources.contains(resourceId)) {
throw new ResourceManager.ResourceLockException(resourceId, lockOwner, "updateData");
}
// Perform update
dataStore.put(resourceId, data);
System.out.println("Data updated for resource: " + resourceId);
}
public void lockResource(String resourceId, String lockOwner) {
lockedResources.add(resourceId);
System.out.println("Resource locked: " + resourceId + " by " + lockOwner);
}
public void unlockResource(String resourceId) {
lockedResources.remove(resourceId);
System.out.println("Resource unlocked: " + resourceId);
}
public void addData(String resourceId, String data) {
dataStore.put(resourceId, data);
}
}
public class BestPracticesExample {
// Configure logger
static {
Logger logger = Logger.getLogger(LoggableException.class.getName());
logger.setLevel(Level.ALL);
ConsoleHandler handler = new ConsoleHandler();
handler.setLevel(Level.ALL);
logger.addHandler(handler);
logger.setUseParentHandlers(false);
}
public static void main(String[] args) {
System.out.println("=== Custom Exceptions - Best Practices ===");
DataService dataService = new DataService();
// Add some test data
dataService.addData("res1", "Data for resource 1");
dataService.addData("res2", "Data for resource 2");
// Test 1: Resource not found
System.out.println("\n1. Testing ResourceNotFoundException:");
try {
String data = dataService.getData("non_existent");
} catch (ResourceManager.ResourceNotFoundException e) {
System.err.println("Caught: " + e.getMessage());
System.err.println("Resource ID: " + e.getResourceId());
System.err.println("Resource Type: " + e.getResourceType());
}
// Test 2: Resource lock
System.out.println("\n2. Testing ResourceLockException:");
try {
dataService.lockResource("res1", "User1");
dataService.updateData("res1", "New data", "User2"); // Different owner
} catch (ResourceManager.ResourceNotFoundException | ResourceManager.ResourceLockException e) {
System.err.println("Caught: " + e.getMessage());
if (e instanceof ResourceManager.ResourceLockException) {
ResourceManager.ResourceLockException rle = (ResourceManager.ResourceLockException) e;
System.err.println("Lock owner: " + rle.getLockOwner());
}
} finally {
dataService.unlockResource("res1");
}
// Test 3: Using exception factory
System.out.println("\n3. Testing ExceptionFactory:");
try {
// Simulate database error
throw ExceptionFactory.createDatabaseException("executeQuery",
"SELECT * FROM users",
new SQLException("Connection timeout"));
} catch (LoggableException e) {
System.err.println("Factory-created exception: " + e.getMessage());
e.getMetrics().forEach((k, v) -> System.err.println(" " + k + ": " + v));
}
// Test 4: Network exception
System.out.println("\n4. Testing Network Exception:");
try {
throw ExceptionFactory.createNetworkException("apiCall",
"https://api.example.com/data", 503);
} catch (LoggableException e) {
System.err.println("Network exception: " + e.getMessage());
}
// Test 5: Validation exception
System.out.println("\n5. Testing Validation Exception:");
try {
throw ExceptionFactory.createValidationException("validateUser",
"email", "invalid-email", "must be valid email format");
} catch (LoggableException e) {
System.err.println("Validation exception: " + e.getMessage());
}
// Test 6: Exception chaining and cause
System.out.println("\n6. Testing Exception Chaining:");
try {
try {
// Simulate low-level exception
throw new IOException("File not found");
} catch (IOException e) {
// Wrap in custom exception
throw new LoggableException("FileService", "readFile",
"Failed to read configuration file", e)
.addMetric("filePath", "/etc/config/app.conf")
.addMetric("attempt", 1);
}
} catch (LoggableException e) {
System.err.println("High-level exception: " + e.getMessage());
System.err.println("Root cause: " + e.getCause().getMessage());
System.err.println("Component: " + e.getComponent());
System.err.println("Operation: " + e.getOperation());
}
System.out.println("\n=== Check console for logged exceptions ===");
}
// Mock SQLException for example
static class SQLException extends Exception {
public SQLException(String message) {
super(message);
}
}
}
9. Conclusion
Key Takeaways:
- Custom Exception Benefits:
- Application-specific error handling
- Better error categorization
- Additional context and information
- Improved code readability
- Exception Types:
- Checked: Must be declared (extend
Exception) - Unchecked: Don't require declaration (extend
RuntimeException)
- Best Practices:
- Provide meaningful error messages
- Include constructors for cause and message
- Add relevant context information
- Follow naming conventions (
*Exceptionsuffix) - Use appropriate exception types (checked vs unchecked)
When to Use Custom Exceptions:
- ✅ Business logic violations (e.g.,
InsufficientFundsException) - ✅ Domain-specific errors (e.g.,
ProductNotFoundException) - ✅ Validation failures (e.g.,
InvalidEmailException) - ✅ System integration errors (e.g.,
PaymentFailedException)
Best Practices Summary:
- Naming: Use descriptive names ending with "Exception"
- Constructors: Provide multiple constructors (message, cause, both)
- Context: Include relevant information in exceptions
- Hierarchy: Create meaningful exception hierarchies
- Documentation: Document when and why to use each exception
- Logging: Integrate with logging frameworks appropriately
Common Anti-Patterns to Avoid:
- ❌ Creating too many specific exceptions
- ❌ Using exceptions for control flow
- ❌ Ignoring or swallowing exceptions
- ❌ Creating overly complex exception hierarchies
- ❌ Not providing sufficient context in exceptions
Final Thoughts:
Custom exceptions are a powerful tool for creating robust, maintainable Java applications. They provide:
- Clear communication of error conditions
- Structured error handling throughout your application
- Better debugging and troubleshooting capabilities
- Consistent error reporting to clients
Master custom exceptions to build more reliable and maintainable Java applications!