Skip to content

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?

  1. Domain-specific errors: Represent business logic violations
  2. Better error categorization: Group related errors
  3. Additional context: Include extra information about the error
  4. 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

  1. Meaningful names: Use names that describe the exceptional condition
  2. Provide constructors: Include common Exception constructors
  3. Add relevant context: Include fields that help diagnose the problem
  4. Override getMessage(): Provide detailed, contextual error messages
  5. 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.