Java Custom Exceptions

Create your own exception classes to handle specific business logic errors - because sometimes built-in exceptions just aren't enough!

What are Custom Exceptions?

Custom exceptions (also called user-defined exceptions) are exception classes that you create yourself to handle specific error conditions in your application. While Java provides many built-in exceptions, sometimes you need exceptions that match your business logic.

Think of it like creating custom warning signs for your specific workplace - generic "Danger" signs exist, but "Caution: Molten Chocolate" is more helpful in a chocolate factory!

  • Why create custom exceptions? Handle application-specific errors meaningfully
  • Better clarity: Exception names describe exactly what went wrong
  • Better organization: Group related errors under custom exception types

💡 Real-World Examples: InvalidAgeException, InsufficientBalanceException, InvalidCredentialsException - these describe specific business problems!

How to Create Custom Exceptions

To create a custom exception, simply extend the Exception class (for checked exceptions) or RuntimeException class (for unchecked exceptions).

  • Extend Exception: Creates a checked exception (must be handled)
  • Extend RuntimeException: Creates an unchecked exception (optional handling)
  • Add constructors: Accept custom error messages
  • Name convention: Always end with "Exception" (e.g., InvalidAgeException)

Creating a Simple Custom Exception

Let's create a custom exception for age validation:

Example: Basic Custom Exception

// Custom Exception Class
class InvalidAgeException extends Exception {
    public InvalidAgeException(String message) {
        super(message);  // Pass message to parent Exception class
    }
}

// Main Class
public class Main {
    
    static void validateAge(int age) throws InvalidAgeException {
        if (age < 18) {
            throw new InvalidAgeException("Age must be 18 or above!");
        }
        System.out.println("Age is valid. Welcome!");
    }
    
    public static void main(String[] args) {
        try {
            validateAge(15);  // This will throw our custom exception
        } catch (InvalidAgeException e) {
            System.out.println("Error: " + e.getMessage());
        }
    }
}
Output:
Error: Age must be 18 or above!

Explanation:

  • class InvalidAgeException extends Exception - Creates custom exception by extending Exception
  • super(message) - Calls parent constructor to set error message
  • throws InvalidAgeException - Method declares it might throw this exception
  • throw new InvalidAgeException(...) - Actually throws the exception with custom message

Custom Exception with Multiple Constructors

Let's create a more robust custom exception with multiple constructors:

Example: Custom Exception with Multiple Constructors

// Enhanced Custom Exception
class InsufficientBalanceException extends Exception {
    
    // Constructor with message only
    public InsufficientBalanceException(String message) {
        super(message);
    }
    
    // Constructor with message and cause
    public InsufficientBalanceException(String message, Throwable cause) {
        super(message, cause);
    }
    
    // Default constructor
    public InsufficientBalanceException() {
        super("Insufficient balance in account!");
    }
}

// Bank Account Class
class BankAccount {
    private double balance;
    
    public BankAccount(double balance) {
        this.balance = balance;
    }
    
    public void withdraw(double amount) throws InsufficientBalanceException {
        if (amount > balance) {
            throw new InsufficientBalanceException(
                "Cannot withdraw $" + amount + ". Available balance: $" + balance
            );
        }
        balance -= amount;
        System.out.println("Withdrawal successful! Remaining balance: $" + balance);
    }
}

// Main Class
public class Main {
    public static void main(String[] args) {
        BankAccount account = new BankAccount(5000);
        
        try {
            account.withdraw(3000);  // Success
            account.withdraw(3000);  // Will throw exception
        } catch (InsufficientBalanceException e) {
            System.out.println("Transaction Failed: " + e.getMessage());
        }
    }
}
Output:
Withdrawal successful! Remaining balance: $2000.0
Transaction Failed: Cannot withdraw $3000.0. Available balance: $2000.0

Explanation:

  • Multiple constructors provide flexibility in creating exception objects
  • Constructor with message and cause helps in exception chaining
  • Default constructor provides a standard error message
  • Exception message includes helpful details like amounts and balance

Types of Custom Exceptions

1. Checked Custom Exceptions

Created by extending Exception class. Must be declared with throws and handled with try-catch. Use for recoverable errors that the caller should know about.

Examples: InvalidCredentialsException, FileProcessingException, DatabaseConnectionException

2. Unchecked Custom Exceptions

Created by extending RuntimeException class. No need to declare with throws. Use for programming errors or conditions that should have been prevented.

Examples: InvalidInputException, NegativeAmountException, EmptyCartException

Unchecked Custom Exception Example

Here's an unchecked custom exception that extends RuntimeException:

Example: Unchecked Custom Exception

// Unchecked Custom Exception
class InvalidProductException extends RuntimeException {
    public InvalidProductException(String message) {
        super(message);
    }
}

// Product Class
class Product {
    private String name;
    private double price;
    
    public Product(String name, double price) {
        if (name == null || name.trim().isEmpty()) {
            throw new InvalidProductException("Product name cannot be empty!");
        }
        if (price <= 0) {
            throw new InvalidProductException("Product price must be positive!");
        }
        this.name = name;
        this.price = price;
    }
    
    public void display() {
        System.out.println("Product: " + name + ", Price: $" + price);
    }
}

// Main Class
public class Main {
    public static void main(String[] args) {
        try {
            Product p1 = new Product("Laptop", 50000);
            p1.display();
            
            Product p2 = new Product("", 100);  // Invalid product
            
        } catch (InvalidProductException e) {
            System.out.println("Error: " + e.getMessage());
        }
    }
}
Output:
Product: Laptop, Price: $50000.0
Error: Product name cannot be empty!

Explanation:

  • extends RuntimeException - Makes it an unchecked exception
  • No throws clause needed in method signature
  • Handling with try-catch is optional (but recommended)
  • Perfect for validation that should have been done earlier

Real-World Example: Student Admission System

Here's a complete example with multiple custom exceptions:

Example: Multiple Custom Exceptions

// Custom Exception 1
class InvalidAgeException extends Exception {
    public InvalidAgeException(String message) {
        super(message);
    }
}

// Custom Exception 2
class InvalidMarksException extends Exception {
    public InvalidMarksException(String message) {
        super(message);
    }
}

// Student Class
class Student {
    private String name;
    private int age;
    private double marks;
    
    public void validateAdmission(String name, int age, double marks) 
            throws InvalidAgeException, InvalidMarksException {
        
        // Validate age
        if (age < 18 || age > 25) {
            throw new InvalidAgeException(
                "Age must be between 18 and 25. Given age: " + age
            );
        }
        
        // Validate marks
        if (marks < 0 || marks > 100) {
            throw new InvalidMarksException(
                "Marks must be between 0 and 100. Given marks: " + marks
            );
        }
        
        if (marks < 40) {
            throw new InvalidMarksException(
                "Minimum 40% marks required. Your marks: " + marks + "%"
            );
        }
        
        this.name = name;
        this.age = age;
        this.marks = marks;
        
        System.out.println("✓ Admission approved for " + name);
        System.out.println("  Age: " + age + ", Marks: " + marks + "%");
    }
}

// Main Class
public class Main {
    public static void main(String[] args) {
        Student student = new Student();
        
        // Test Case 1: Valid student
        try {
            student.validateAdmission("Rahul", 20, 85);
        } catch (InvalidAgeException | InvalidMarksException e) {
            System.out.println("✗ " + e.getMessage());
        }
        
        System.out.println();
        
        // Test Case 2: Invalid age
        try {
            student.validateAdmission("Priya", 17, 75);
        } catch (InvalidAgeException | InvalidMarksException e) {
            System.out.println("✗ " + e.getMessage());
        }
        
        System.out.println();
        
        // Test Case 3: Insufficient marks
        try {
            student.validateAdmission("Amit", 22, 35);
        } catch (InvalidAgeException | InvalidMarksException e) {
            System.out.println("✗ " + e.getMessage());
        }
    }
}
Output:
✓ Admission approved for Rahul
  Age: 20, Marks: 85.0%

✗ Age must be between 18 and 25. Given age: 17

✗ Minimum 40% marks required. Your marks: 35.0%

Explanation:

  • Two custom exceptions handle different validation scenarios
  • Method throws multiple exceptions using comma separation
  • Multi-catch block handles both exceptions: InvalidAgeException | InvalidMarksException
  • Each exception provides specific, meaningful error messages
  • Code is clean, readable, and handles all edge cases properly

Custom Exception with Additional Data

You can add extra fields and methods to custom exceptions for more information:

Example: Enhanced Custom Exception

// Enhanced Custom Exception with extra data
class InsufficientFundsException extends Exception {
    private double balance;
    private double requiredAmount;
    private double shortfall;
    
    public InsufficientFundsException(double balance, double requiredAmount) {
        super("Insufficient funds for transaction");
        this.balance = balance;
        this.requiredAmount = requiredAmount;
        this.shortfall = requiredAmount - balance;
    }
    
    public double getBalance() {
        return balance;
    }
    
    public double getRequiredAmount() {
        return requiredAmount;
    }
    
    public double getShortfall() {
        return shortfall;
    }
    
    public String getDetailedMessage() {
        return String.format(
            "Transaction failed!\n" +
            "  Current Balance: $%.2f\n" +
            "  Amount Required: $%.2f\n" +
            "  Shortfall: $%.2f",
            balance, requiredAmount, shortfall
        );
    }
}

// Main Class
public class Main {
    
    static void processPayment(double balance, double amount) 
            throws InsufficientFundsException {
        if (amount > balance) {
            throw new InsufficientFundsException(balance, amount);
        }
        System.out.println("Payment of $" + amount + " processed successfully!");
    }
    
    public static void main(String[] args) {
        try {
            processPayment(5000, 7500);
        } catch (InsufficientFundsException e) {
            System.out.println(e.getDetailedMessage());
            System.out.println("\nPlease add at least $" + e.getShortfall() + " to proceed.");
        }
    }
}
Output:
Transaction failed!
  Current Balance: $5000.00
  Amount Required: $7500.00
  Shortfall: $2500.00

Please add at least $2500.0 to proceed.

Explanation:

  • Custom exception stores additional data: balance, required amount, shortfall
  • Getter methods provide access to the stored data
  • getDetailedMessage() creates formatted, user-friendly error details
  • Exception object becomes a data carrier with helpful information
  • Makes debugging and user feedback much better

Best Practices for Custom Exceptions

  • Meaningful names: Name should clearly describe the error (InvalidEmailException, not MyException)
  • Always end with "Exception": Follow Java naming conventions
  • Extend appropriate class: Exception for checked, RuntimeException for unchecked
  • Provide constructors: At least message constructor, optionally message + cause
  • Add helpful details: Include relevant data that helps understand the error
  • Don't overuse: Create custom exceptions only when built-in ones don't fit
  • Document well: Add Javadoc comments explaining when to throw this exception
  • Keep them simple: Don't add unnecessary complexity

⚠️ When NOT to Create Custom Exceptions: If a built-in exception like IllegalArgumentException or IllegalStateException fits your need, use it instead!

Checked vs Unchecked Custom Exceptions

Aspect Checked (extends Exception) Unchecked (extends RuntimeException)
Handling Must be handled (forced by compiler) Optional handling
throws Declaration Required in method signature Not required
Use For Recoverable conditions caller should know about Programming errors, validation failures
Examples InvalidCredentialsException, DatabaseException InvalidInputException, NullProductException

Key Takeaways

  • Custom exceptions: Create specific exceptions for your application's needs
  • Extend Exception: For checked custom exceptions (must handle)
  • Extend RuntimeException: For unchecked custom exceptions (optional handling)
  • Meaningful names: Use descriptive names ending with "Exception"
  • Add constructors: Provide flexibility with multiple constructors
  • Include data: Store relevant information to help debugging
  • Better code: Makes error handling more readable and maintainable

🎯 Remember: Custom exceptions make your code speak your business language. Instead of generic exceptions, use meaningful names that describe exactly what went wrong!