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!