Java Programming Exception Handling
Error Management

Java Exception Handling - Complete Tutorial

Master Java exception handling: Learn try-catch-finally blocks, throw, throws, custom exceptions, exception hierarchy, and best practices for robust error management.

Exception Types

Checked & Unchecked

Try-Catch-Finally

Error handling blocks

Throw/Throws

Exception propagation

Custom Exceptions

User-defined errors

1. Introduction to Exception Handling

Exception handling in Java is a powerful mechanism to handle runtime errors so that normal flow of the application can be maintained. An exception is an event that disrupts the normal flow of the program.

Why Exception Handling?
  • Maintain Normal Flow: Prevent application crashes
  • Meaningful Error Messages: Provide user-friendly errors
  • Resource Management: Ensure proper resource cleanup
  • Debugging: Easier identification of error sources
  • Robust Applications: Create fault-tolerant software
  • Separation of Concerns: Separate error handling from business logic
Key Terminology
  • Exception: Abnormal condition disrupting program flow
  • Error: Serious problem beyond program control
  • Throwable: Superclass of all exceptions and errors
  • Checked Exception: Must be handled at compile time
  • Unchecked Exception: Runtime exceptions
  • Try-Catch: Blocks to handle exceptions

Exception Handling Flow

When an exception occurs: JVM creates exception object → Looks for handler → If found, executes handler → If not, terminates program. Proper handling prevents termination.

Exception Hierarchy
                    Throwable
                    ├── Error (Serious, Unchecked)
                    │   ├── VirtualMachineError
                    │   │   ├── OutOfMemoryError
                    │   │   └── StackOverflowError
                    │   └── ...
                    │
                    └── Exception
                        ├── RuntimeException (Unchecked)
                        │   ├── NullPointerException
                        │   ├── ArithmeticException
                        │   ├── ArrayIndexOutOfBoundsException
                        │   ├── IllegalArgumentException
                        │   └── ...
                        │
                        └── Checked Exceptions
                            ├── IOException
                            ├── SQLException
                            ├── ClassNotFoundException
                            └── ...

2. Exception Hierarchy and Types

Java exceptions are organized in a hierarchy with Throwable at the top. Understanding this hierarchy is crucial for effective exception handling.

Exception Type Superclass Checked/Unchecked When It Occurs Common Examples
Error Throwable Unchecked Serious system problems OutOfMemoryError, StackOverflowError
RuntimeException Exception Unchecked Programming mistakes NullPointerException, ArithmeticException
Checked Exceptions Exception (excluding RuntimeException) Checked External conditions IOException, SQLException
Checked Exceptions
  • Checked at compile-time
  • Must be handled using try-catch or declared with throws
  • Represent recoverable conditions
  • Subclasses of Exception (excluding RuntimeException)
  • Examples: IOException, SQLException, ClassNotFoundException
  • Client should recover from these
Unchecked Exceptions
  • Checked at runtime
  • Not required to be handled or declared
  • Represent programming bugs
  • Subclasses of RuntimeException and Error
  • Examples: NullPointerException, ArrayIndexOutOfBoundsException
  • Client cannot reasonably recover
CommonExceptionsExample.java
public class CommonExceptionsExample {
    public static void main(String[] args) {
        System.out.println("=== Common Java Exceptions ===");
        
        // 1. NullPointerException (Unchecked)
        try {
            String str = null;
            System.out.println("Length: " + str.length()); // This will throw NPE
        } catch (NullPointerException e) {
            System.out.println("1. NullPointerException caught: " + e.getMessage());
        }
        
        // 2. ArithmeticException (Unchecked)
        try {
            int result = 10 / 0; // Division by zero
        } catch (ArithmeticException e) {
            System.out.println("2. ArithmeticException caught: " + e.getMessage());
        }
        
        // 3. ArrayIndexOutOfBoundsException (Unchecked)
        try {
            int[] numbers = {1, 2, 3};
            System.out.println(numbers[5]); // Invalid index
        } catch (ArrayIndexOutOfBoundsException e) {
            System.out.println("3. ArrayIndexOutOfBoundsException caught: " + e.getMessage());
        }
        
        // 4. NumberFormatException (Unchecked)
        try {
            String invalidNumber = "abc123";
            int num = Integer.parseInt(invalidNumber);
        } catch (NumberFormatException e) {
            System.out.println("4. NumberFormatException caught: " + e.getMessage());
        }
        
        // 5. IllegalArgumentException (Unchecked)
        try {
            setAge(-5);
        } catch (IllegalArgumentException e) {
            System.out.println("5. IllegalArgumentException caught: " + e.getMessage());
        }
        
        // 6. ClassCastException (Unchecked)
        try {
            Object obj = "Hello";
            Integer num = (Integer) obj; // Invalid cast
        } catch (ClassCastException e) {
            System.out.println("6. ClassCastException caught: " + e.getMessage());
        }
        
        // 7. StringIndexOutOfBoundsException (Unchecked)
        try {
            String text = "Hello";
            char ch = text.charAt(10); // Invalid string index
        } catch (StringIndexOutOfBoundsException e) {
            System.out.println("7. StringIndexOutOfBoundsException caught: " + e.getMessage());
        }
        
        // 8. NegativeArraySizeException (Unchecked)
        try {
            int[] arr = new int[-5]; // Negative array size
        } catch (NegativeArraySizeException e) {
            System.out.println("8. NegativeArraySizeException caught: " + e.getMessage());
        }
        
        System.out.println("\nProgram continues normally after handling exceptions!");
    }
    
    private static void setAge(int age) {
        if (age < 0) {
            throw new IllegalArgumentException("Age cannot be negative: " + age);
        }
        System.out.println("Age set to: " + age);
    }
}

3. Try-Catch-Finally Blocks

The try-catch-finally blocks are the core of Java exception handling. They allow you to write code that might throw exceptions and handle them gracefully.

Try-Catch-Finally Flow
                    Start
                      │
                      ▼
                  ┌─────────┐
                  │  try    │ ←── Exception occurs here
                  └────┬────┘
                       │
        ┌──────────────┼──────────────┐
        │              │              │
        ▼              ▼              ▼
    Exception   Exception   No Exception
       A           B           │
       │           │           │
       └─────┬─────┘           │
             │                 │
             ▼                 ▼
    ┌─────────────────┐   ┌─────────┐
    │   catch block   │   │finally  │
    │   (handles)     │   │ block   │
    └────────┬────────┘   └────┬────┘
             │                 │
             └────────┬────────┘
                      │
                      ▼
                 Continue program
TryCatchFinallyExample.java
import java.util.Scanner;

public class TryCatchFinallyExample {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        
        System.out.println("=== Try-Catch-Finally Examples ===");
        
        // Example 1: Basic try-catch
        System.out.println("\n--- Example 1: Basic Try-Catch ---");
        try {
            System.out.print("Enter numerator: ");
            int numerator = scanner.nextInt();
            System.out.print("Enter denominator: ");
            int denominator = scanner.nextInt();
            
            int result = numerator / denominator;
            System.out.println("Result: " + result);
        } catch (ArithmeticException e) {
            System.out.println("Error: Division by zero is not allowed!");
        } catch (Exception e) {
            System.out.println("Unexpected error: " + e.getMessage());
        }
        
        // Example 2: Multiple catch blocks
        System.out.println("\n--- Example 2: Multiple Catch Blocks ---");
        try {
            System.out.print("Enter array size: ");
            int size = scanner.nextInt();
            int[] array = new int[size];
            
            System.out.print("Enter index to access: ");
            int index = scanner.nextInt();
            
            System.out.print("Enter value to store: ");
            String value = scanner.next();
            
            array[index] = Integer.parseInt(value);
            System.out.println("Value stored successfully!");
            
        } catch (NegativeArraySizeException e) {
            System.out.println("Error: Array size cannot be negative!");
        } catch (ArrayIndexOutOfBoundsException e) {
            System.out.println("Error: Invalid array index!");
        } catch (NumberFormatException e) {
            System.out.println("Error: Invalid number format!");
        } catch (Exception e) {
            System.out.println("General error: " + e.getMessage());
        }
        
        // Example 3: Try-catch-finally with resources
        System.out.println("\n--- Example 3: Try-Catch-Finally ---");
        Scanner fileScanner = null;
        try {
            System.out.print("Enter filename to open: ");
            String filename = scanner.next();
            
            // Simulating file operations
            if (filename.equals("missing.txt")) {
                throw new RuntimeException("File not found: " + filename);
            }
            
            System.out.println("Processing file: " + filename);
            // File processing logic here
            
        } catch (RuntimeException e) {
            System.out.println("File error: " + e.getMessage());
        } finally {
            // This block ALWAYS executes
            System.out.println("Finally block executed - cleaning up resources");
            if (fileScanner != null) {
                fileScanner.close();
            }
        }
        
        // Example 4: Nested try-catch
        System.out.println("\n--- Example 4: Nested Try-Catch ---");
        try {
            // Outer try block
            System.out.println("Outer try block started");
            
            try {
                // Inner try block
                System.out.print("Enter a number: ");
                int num = scanner.nextInt();
                
                if (num < 0) {
                    throw new IllegalArgumentException("Negative numbers not allowed");
                }
                
                System.out.println("Square root: " + Math.sqrt(num));
                
            } catch (IllegalArgumentException e) {
                System.out.println("Inner catch: " + e.getMessage());
                throw e; // Re-throwing exception
            } finally {
                System.out.println("Inner finally executed");
            }
            
            System.out.println("Outer try block completed");
            
        } catch (Exception e) {
            System.out.println("Outer catch: " + e.getClass().getSimpleName());
        } finally {
            System.out.println("Outer finally executed");
        }
        
        // Example 5: Try-with-resources (Java 7+)
        System.out.println("\n--- Example 5: Try-With-Resources ---");
        try (Scanner autoCloseScanner = new Scanner(System.in)) {
            System.out.print("Enter your name: ");
            String name = autoCloseScanner.nextLine();
            System.out.println("Hello, " + name + "!");
            
            // Scanner auto-closes here
        } catch (Exception e) {
            System.out.println("Error: " + e.getMessage());
        }
        
        System.out.println("\n=== Program Completed Successfully ===");
        scanner.close();
    }
    
    // Helper method demonstrating exception propagation
    private static void riskyMethod() throws Exception {
        System.out.println("Executing risky method...");
        if (Math.random() > 0.5) {
            throw new Exception("Something went wrong!");
        }
        System.out.println("Risky method completed successfully");
    }
}
Block Purpose When It Executes Required? Multiple Allowed?
try Contains code that might throw exceptions Always (if no exception before) Yes No (but can be nested)
catch Handles specific exceptions Only when matching exception occurs No (but try must have catch or finally) Yes (multiple catch blocks)
finally Cleanup code (always executes) Always (except System.exit()) No No (only one finally per try)
Catch Block Ordering:
  • Always catch more specific exceptions first
  • More general exceptions (Exception) should come last
  • Compiler error if specific exception comes after general one
  • Example: catch(ArithmeticException e) before catch(Exception e)
  • Unreachable catch blocks cause compilation errors

4. Throw and Throws Keywords

The throw keyword is used to explicitly throw an exception, while throws is used in method signatures to declare exceptions that the method might throw.

ThrowExample.java
public class ThrowExample {
    
    // Method that throws exception based on condition
    public static void checkAge(int age) {
        if (age < 18) {
            // Explicitly throwing an exception
            throw new IllegalArgumentException(
                "Age must be 18 or older. Provided: " + age
            );
        }
        System.out.println("Age verified: " + age);
    }
    
    // Method that throws different types of exceptions
    public static void processInput(String input, int divisor) {
        if (input == null || input.trim().isEmpty()) {
            throw new NullPointerException("Input cannot be null or empty");
        }
        
        if (divisor == 0) {
            throw new ArithmeticException("Divisor cannot be zero");
        }
        
        if (!input.matches("\\d+")) {
            throw new NumberFormatException("Input must be a number: " + input);
        }
        
        int number = Integer.parseInt(input);
        System.out.println("Result: " + (number / divisor));
    }
    
    // Method that creates and throws custom exception
    public static void withdrawMoney(double balance, double amount) {
        if (amount <= 0) {
            throw new IllegalArgumentException(
                "Withdrawal amount must be positive: " + amount
            );
        }
        
        if (amount > balance) {
            // Creating exception with detailed message
            InsufficientFundsException exception = 
                new InsufficientFundsException(balance, amount);
            throw exception;
        }
        
        balance -= amount;
        System.out.println("Withdrawal successful. New balance: " + balance);
    }
    
    // Method that re-throws exception
    public static void processWithRetry(int attempts) {
        for (int i = 1; i <= attempts; i++) {
            try {
                System.out.println("Attempt " + i + " of " + attempts);
                riskyOperation();
                System.out.println("Operation successful!");
                return; // Exit if successful
            } catch (RuntimeException e) {
                System.out.println("Attempt " + i + " failed: " + e.getMessage());
                
                if (i == attempts) {
                    // Re-throw the exception on last attempt
                    throw new RuntimeException(
                        "All " + attempts + " attempts failed", e
                    );
                }
                
                // Wait before retry
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException ie) {
                    Thread.currentThread().interrupt();
                }
            }
        }
    }
    
    private static void riskyOperation() {
        if (Math.random() < 0.7) {
            throw new RuntimeException("Random operation failure");
        }
    }
    
    public static void main(String[] args) {
        System.out.println("=== Throw Keyword Examples ===");
        
        // Example 1: Basic throw
        try {
            checkAge(16); // This will throw exception
        } catch (IllegalArgumentException e) {
            System.out.println("Caught: " + e.getMessage());
        }
        
        // Example 2: Multiple throw scenarios
        try {
            processInput("abc", 5); // Will throw NumberFormatException
        } catch (Exception e) {
            System.out.println("Caught: " + e.getClass().getSimpleName() + 
                             " - " + e.getMessage());
        }
        
        // Example 3: Custom exception throw
        try {
            withdrawMoney(1000.0, 1500.0); // Insufficient funds
        } catch (InsufficientFundsException e) {
            System.out.println("Caught: " + e.getMessage());
            System.out.println("Required: " + e.getRequiredAmount());
            System.out.println("Available: " + e.getAvailableBalance());
        }
        
        // Example 4: Re-throw with wrapper
        try {
            processWithRetry(3);
        } catch (RuntimeException e) {
            System.out.println("Final error: " + e.getMessage());
            System.out.println("Original cause: " + e.getCause().getMessage());
        }
        
        // Example 5: Throw in constructor
        try {
            Person p = new Person("", 25); // Empty name
        } catch (IllegalArgumentException e) {
            System.out.println("Person creation failed: " + e.getMessage());
        }
        
        System.out.println("\nProgram continues after exception handling");
    }
}

// Custom exception class
class InsufficientFundsException extends RuntimeException {
    private double availableBalance;
    private double requiredAmount;
    
    public InsufficientFundsException(double balance, double amount) {
        super("Insufficient funds. Available: " + balance + ", Required: " + amount);
        this.availableBalance = balance;
        this.requiredAmount = amount;
    }
    
    public double getAvailableBalance() {
        return availableBalance;
    }
    
    public double getRequiredAmount() {
        return requiredAmount;
    }
}

// Class demonstrating throw in constructor
class Person {
    private String name;
    private int age;
    
    public Person(String name, int age) {
        if (name == null || name.trim().isEmpty()) {
            throw new IllegalArgumentException("Name cannot be null or empty");
        }
        if (age < 0 || age > 150) {
            throw new IllegalArgumentException("Invalid age: " + age);
        }
        this.name = name;
        this.age = age;
    }
}
ThrowsExample.java
import java.io.*;
import java.net.URL;
import java.net.MalformedURLException;
import java.sql.*;

public class ThrowsExample {
    
    // Method declaring checked exceptions with throws
    public static void readFile(String filename) 
            throws FileNotFoundException, IOException {
        
        System.out.println("Reading file: " + filename);
        
        // This can throw FileNotFoundException
        FileReader fileReader = new FileReader(filename);
        BufferedReader reader = new BufferedReader(fileReader);
        
        try {
            String line;
            while ((line = reader.readLine()) != null) {
                System.out.println(line);
            }
        } finally {
            reader.close();
        }
    }
    
    // Method declaring multiple exceptions
    public static void connectToDatabase(String url, String user, String password)
            throws SQLException, ClassNotFoundException {
        
        System.out.println("Connecting to database...");
        
        // This can throw ClassNotFoundException
        Class.forName("com.mysql.cj.jdbc.Driver");
        
        // This can throw SQLException
        Connection connection = DriverManager.getConnection(url, user, password);
        
        try {
            // Database operations
            Statement statement = connection.createStatement();
            ResultSet resultSet = statement.executeQuery("SELECT * FROM users");
            
            while (resultSet.next()) {
                System.out.println("User: " + resultSet.getString("username"));
            }
            
        } finally {
            connection.close();
        }
    }
    
    // Method with both checked and unchecked exceptions
    public static void processUrl(String urlString) 
            throws MalformedURLException, IOException {
        
        System.out.println("Processing URL: " + urlString);
        
        // Can throw MalformedURLException (checked)
        URL url = new URL(urlString);
        
        // Can throw IOException (checked)
        BufferedReader reader = new BufferedReader(
            new InputStreamReader(url.openStream())
        );
        
        try {
            String line;
            int lineCount = 0;
            while ((line = reader.readLine()) != null && lineCount < 10) {
                System.out.println("Line " + (++lineCount) + ": " + line);
            }
        } finally {
            reader.close();
        }
    }
    
    // Method that doesn't handle exceptions but declares them
    public static void riskyOperation() 
            throws IOException, SQLException, ClassNotFoundException {
        
        double random = Math.random();
        
        if (random < 0.33) {
            throw new IOException("Simulated IO error");
        } else if (random < 0.66) {
            throw new SQLException("Simulated database error");
        } else {
            throw new ClassNotFoundException("Simulated class not found");
        }
    }
    
    // Method with throws in interface implementation
    interface DataProcessor {
        void process() throws IOException, SQLException;
    }
    
    static class FileProcessor implements DataProcessor {
        @Override
        public void process() throws IOException {
            // Can only throw IOException or its subclasses
            // Cannot throw SQLException even though interface declares it
            throw new IOException("File processing error");
        }
    }
    
    // Main method with throws declaration
    public static void main(String[] args) 
            throws IOException, SQLException, ClassNotFoundException {
        
        System.out.println("=== Throws Keyword Examples ===");
        
        // Example 1: Handling declared exceptions
        try {
            readFile("test.txt");
        } catch (FileNotFoundException e) {
            System.out.println("File not found: " + e.getMessage());
        } catch (IOException e) {
            System.out.println("IO Error: " + e.getMessage());
        }
        
        // Example 2: Propagating exceptions
        try {
            connectToDatabase(
                "jdbc:mysql://localhost:3306/mydb",
                "root",
                "password"
            );
        } catch (SQLException | ClassNotFoundException e) {
            System.out.println("Database error: " + e.getClass().getSimpleName());
            // Re-throw with additional context
            throw new RuntimeException("Database operation failed", e);
        }
        
        // Example 3: Multiple exception handling
        String[] urls = {
            "https://example.com",
            "invalid-url",  // Will cause MalformedURLException
            "https://google.com"
        };
        
        for (String url : urls) {
            try {
                processUrl(url);
            } catch (MalformedURLException e) {
                System.out.println("Invalid URL: " + url + " - " + e.getMessage());
            } catch (IOException e) {
                System.out.println("IO Error processing URL: " + e.getMessage());
            }
        }
        
        // Example 4: Using method with throws
        try {
            riskyOperation();
        } catch (IOException | SQLException | ClassNotFoundException e) {
            System.out.println("Caught: " + e.getClass().getSimpleName());
            // Can choose to handle or re-throw
        }
        
        // Example 5: Method overriding with throws
        DataProcessor processor = new FileProcessor();
        try {
            processor.process();
        } catch (IOException e) {
            System.out.println("Processor error: " + e.getMessage());
        }
        
        System.out.println("\n=== Program Completed ===");
    }
    
    // Helper method demonstrating exception propagation chain
    public static void methodA() throws IOException {
        System.out.println("Method A calling Method B");
        methodB();  // Exception propagates up
    }
    
    public static void methodB() throws IOException {
        System.out.println("Method B calling Method C");
        methodC();  // Exception propagates up
    }
    
    public static void methodC() throws IOException {
        System.out.println("Method C throwing exception");
        throw new IOException("Error originated in Method C");
    }
}
Keyword Purpose Used In Exception Type Example
throw Explicitly throw an exception object Method body Any Throwable throw new IOException();
throws Declare exceptions method might throw Method signature Checked exceptions void read() throws IOException
Throws vs Try-Catch:
  • Use throws when the current method cannot handle the exception
  • Use try-catch when you can handle the exception locally
  • Checked exceptions must be either caught or declared with throws
  • Unchecked exceptions can be thrown without declaration
  • Overriding methods can throw same or narrower exceptions than parent
  • Overriding methods cannot throw broader checked exceptions

5. Creating Custom Exceptions

Custom exceptions allow you to create application-specific exception types that provide meaningful error information for your domain.

CustomExceptionsExample.java
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;

public class CustomExceptionsExample {
    
    public static void main(String[] args) {
        System.out.println("=== Custom Exception Examples ===");
        
        // Example 1: Basic custom exception
        System.out.println("\n--- Example 1: Authentication ---");
        try {
            authenticateUser("admin", "wrongpassword");
        } catch (AuthenticationException e) {
            System.out.println("Authentication failed:");
            System.out.println("Error Code: " + e.getErrorCode());
            System.out.println("Message: " + e.getMessage());
            System.out.println("Timestamp: " + e.getTimestamp());
        }
        
        // Example 2: Validation exception with details
        System.out.println("\n--- Example 2: User Registration ---");
        try {
            registerUser("", "weak", "user@example.com", 15);
        } catch (ValidationException e) {
            System.out.println("Validation failed:");
            System.out.println("Errors: " + e.getErrors());
            e.getErrors().forEach((field, error) -> 
                System.out.println(field + ": " + error)
            );
        }
        
        // Example 3: Business logic exception
        System.out.println("\n--- Example 3: Bank Transaction ---");
        try {
            BankAccount account = new BankAccount("ACC001", 500.0);
            account.withdraw(800.0);
        } catch (InsufficientBalanceException e) {
            System.out.println("Transaction failed:");
            System.out.println("Account: " + e.getAccountNumber());
            System.out.println("Available: $" + e.getAvailableBalance());
            System.out.println("Requested: $" + e.getRequestedAmount());
            System.out.println("Shortage: $" + e.getShortageAmount());
        }
        
        // Example 4: Chained custom exceptions
        System.out.println("\n--- Example 4: Order Processing ---");
        try {
            processOrder("ORD123", -2);
        } catch (OrderProcessingException e) {
            System.out.println("Order processing error:");
            System.out.println("Order ID: " + e.getOrderId());
            System.out.println("Error: " + e.getMessage());
            
            if (e.getCause() != null) {
                System.out.println("Root cause: " + e.getCause().getMessage());
            }
        }
        
        // Example 5: Checked custom exception
        System.out.println("\n--- Example 5: File Processing ---");
        try {
            processFile("encrypted.dat");
        } catch (FileEncryptionException e) {
            System.out.println("File processing error:");
            System.out.println("File: " + e.getFileName());
            System.out.println("Error: " + e.getMessage());
            System.out.println("Suggestion: " + e.getRecoverySuggestion());
        }
        
        System.out.println("\n=== All Examples Completed ===");
    }
    
    // Example 1: Basic custom unchecked exception
    static void authenticateUser(String username, String password) {
        if (!"admin".equals(username) || !"admin123".equals(password)) {
            throw new AuthenticationException(
                "INVALID_CREDENTIALS",
                "Invalid username or password"
            );
        }
        System.out.println("Authentication successful for: " + username);
    }
    
    // Example 2: Validation with multiple errors
    static void registerUser(String username, String password, 
                            String email, int age) {
        Map errors = new HashMap<>();
        
        if (username == null || username.trim().isEmpty()) {
            errors.put("username", "Username cannot be empty");
        } else if (username.length() < 3) {
            errors.put("username", "Username must be at least 3 characters");
        }
        
        if (password == null || password.length() < 8) {
            errors.put("password", "Password must be at least 8 characters");
        }
        
        if (email == null || !email.contains("@")) {
            errors.put("email", "Invalid email address");
        }
        
        if (age < 18) {
            errors.put("age", "User must be at least 18 years old");
        }
        
        if (!errors.isEmpty()) {
            throw new ValidationException("User validation failed", errors);
        }
        
        System.out.println("User registered successfully: " + username);
    }
    
    // Example 3: Business logic with custom exception
    static class BankAccount {
        private String accountNumber;
        private double balance;
        
        public BankAccount(String accountNumber, double balance) {
            this.accountNumber = accountNumber;
            this.balance = balance;
        }
        
        public void withdraw(double amount) {
            if (amount <= 0) {
                throw new IllegalArgumentException(
                    "Withdrawal amount must be positive"
                );
            }
            
            if (amount > balance) {
                throw new InsufficientBalanceException(
                    accountNumber, balance, amount
                );
            }
            
            balance -= amount;
            System.out.println("Withdrawn: $" + amount + 
                             ", New balance: $" + balance);
        }
    }
    
    // Example 4: Chained exceptions
    static void processOrder(String orderId, int quantity) {
        try {
            if (quantity <= 0) {
                throw new IllegalArgumentException(
                    "Quantity must be positive: " + quantity
                );
            }
            
            // Simulate inventory check
            if (quantity > 100) {
                throw new RuntimeException("Insufficient inventory");
            }
            
            System.out.println("Processing order: " + orderId);
            
        } catch (Exception e) {
            // Wrap the exception with custom exception
            throw new OrderProcessingException(
                orderId,
                "Failed to process order: " + orderId,
                e  // Preserve original cause
            );
        }
    }
    
    // Example 5: Checked custom exception
    static void processFile(String fileName) throws FileEncryptionException {
        try {
            // Simulate file processing
            if (fileName.endsWith(".encrypted")) {
                throw new IOException("Decryption key not found");
            }
            
            System.out.println("Processing file: " + fileName);
            
        } catch (IOException e) {
            throw new FileEncryptionException(
                fileName,
                "Failed to process encrypted file",
                e,
                "Please provide decryption key or use unencrypted file"
            );
        }
    }
}

// ========== CUSTOM EXCEPTION CLASSES ==========

// 1. Basic custom unchecked exception
class AuthenticationException extends RuntimeException {
    private String errorCode;
    private LocalDateTime timestamp;
    
    public AuthenticationException(String errorCode, String message) {
        super(message);
        this.errorCode = errorCode;
        this.timestamp = LocalDateTime.now();
    }
    
    public String getErrorCode() {
        return errorCode;
    }
    
    public LocalDateTime getTimestamp() {
        return timestamp;
    }
}

// 2. Validation exception with error details
class ValidationException extends RuntimeException {
    private Map errors;
    
    public ValidationException(String message, Map errors) {
        super(message);
        this.errors = errors;
    }
    
    public Map getErrors() {
        return errors;
    }
    
    @Override
    public String toString() {
        return super.toString() + ", Errors: " + errors;
    }
}

// 3. Business exception with account details
class InsufficientBalanceException extends RuntimeException {
    private String accountNumber;
    private double availableBalance;
    private double requestedAmount;
    
    public InsufficientBalanceException(String accountNumber, 
                                      double balance, double amount) {
        super(String.format(
            "Insufficient balance in account %s. Available: $%.2f, Requested: $%.2f",
            accountNumber, balance, amount
        ));
        this.accountNumber = accountNumber;
        this.availableBalance = balance;
        this.requestedAmount = amount;
    }
    
    public String getAccountNumber() {
        return accountNumber;
    }
    
    public double getAvailableBalance() {
        return availableBalance;
    }
    
    public double getRequestedAmount() {
        return requestedAmount;
    }
    
    public double getShortageAmount() {
        return requestedAmount - availableBalance;
    }
}

// 4. Exception with order context
class OrderProcessingException extends RuntimeException {
    private String orderId;
    
    public OrderProcessingException(String orderId, String message) {
        super(message);
        this.orderId = orderId;
    }
    
    public OrderProcessingException(String orderId, String message, Throwable cause) {
        super(message, cause);
        this.orderId = orderId;
    }
    
    public String getOrderId() {
        return orderId;
    }
}

// 5. Checked custom exception with recovery suggestion
class FileEncryptionException extends Exception {
    private String fileName;
    private String recoverySuggestion;
    
    public FileEncryptionException(String fileName, String message) {
        super(message);
        this.fileName = fileName;
    }
    
    public FileEncryptionException(String fileName, String message, 
                                 Throwable cause) {
        super(message, cause);
        this.fileName = fileName;
    }
    
    public FileEncryptionException(String fileName, String message,
                                 Throwable cause, String recoverySuggestion) {
        super(message, cause);
        this.fileName = fileName;
        this.recoverySuggestion = recoverySuggestion;
    }
    
    public String getFileName() {
        return fileName;
    }
    
    public String getRecoverySuggestion() {
        return recoverySuggestion;
    }
}
When to Create Custom Exceptions
  • Domain-specific error conditions
  • Need to include additional information
  • Better error categorization
  • Consistent error handling across application
  • When built-in exceptions don't accurately describe error
  • For business rule violations
Custom Exception Best Practices
  • Extend appropriate superclass (Exception or RuntimeException)
  • Provide meaningful error messages
  • Include serialVersionUID for Serializable exceptions
  • Add constructors matching superclass patterns
  • Include relevant context information
  • Follow naming convention (ends with "Exception")
Custom Exception Anti-patterns:
  • Overuse: Don't create custom exceptions for every minor error
  • Empty exceptions: Always provide meaningful messages
  • Ignoring cause: Preserve original exception with initCause() or constructor
  • Checked for everything: Use runtime exceptions for programming errors
  • Too many fields: Keep exceptions focused and simple

6. Exception Handling Best Practices

Common Exception Handling Mistakes:
  1. Empty catch blocks: catch(Exception e) {}
  2. Catching Throwable/Error: Don't catch serious system errors
  3. Overly broad catch: catch(Exception e) when specific would do
  4. Exception swallowing: Hiding exceptions without logging
  5. Resource leaks: Not closing resources in finally block
  6. Using exceptions for flow control: Exceptions are for exceptional conditions
Exception Handling Best Practices
  • Use specific exceptions over general ones
  • Document exceptions with @throws javadoc
  • Use try-with-resources for auto-closeable resources
  • Clean up resources in finally blocks
  • Include cause exceptions when re-throwing
  • Use runtime exceptions for programming errors
  • Log exceptions appropriately
Performance Considerations
  • Exception creation is expensive - avoid in loops
  • Use precondition checks to avoid exceptions
  • Consider error codes for performance-critical code
  • Cache and reuse exception objects when safe
  • Use boolean return values for expected failures
  • Fill in stack trace only when needed

Exception Handling Principles

Fail Fast: Detect errors as early as possible
Fail Safe: System remains in safe state after failure
Separation of Concerns: Keep error handling separate from business logic
Meaningful Messages: Provide clear, actionable error information
Graceful Degradation: System continues with reduced functionality
Defensive Programming: Assume everything that can go wrong will

BestPracticesExample.java
import java.io.*;
import java.util.logging.*;

public class BestPracticesExample {
    private static final Logger logger = 
        Logger.getLogger(BestPracticesExample.class.getName());
    
    public static void main(String[] args) {
        System.out.println("=== Exception Handling Best Practices ===");
        
        // Practice 1: Specific exceptions over general ones
        try {
            readConfiguration("config.properties");
        } catch (FileNotFoundException e) {
            // Specific handling for missing file
            logger.severe("Configuration file not found: " + e.getMessage());
            System.out.println("Using default configuration");
        } catch (IOException e) {
            // General IO errors
            logger.severe("IO error reading configuration: " + e.getMessage());
        }
        
        // Practice 2: Try-with-resources
        try (BufferedReader reader = new BufferedReader(
                new FileReader("data.txt"))) {
            String line;
            while ((line = reader.readLine()) != null) {
                processLine(line);
            }
        } catch (IOException e) {
            logger.log(Level.SEVERE, "Error reading file", e);
        }
        
        // Practice 3: Never swallow exceptions
        badPracticeSwallowException();
        goodPracticeLogException();
        
        // Practice 4: Use exceptions for exceptional conditions
        validateInputBeforeProcessing("valid input");
        
        // Practice 5: Include cause when re-throwing
        try {
            processData();
        } catch (ProcessingException e) {
            logger.log(Level.SEVERE, "Data processing failed", e);
            // Original cause is preserved
            System.out.println("Root cause: " + e.getCause().getMessage());
        }
        
        // Practice 6: Clean up in finally or try-with-resources
        Connection connection = null;
        try {
            connection = openDatabaseConnection();
            executeQuery(connection);
        } catch (SQLException e) {
            logger.severe("Database error: " + e.getMessage());
        } finally {
            // Always close resources
            if (connection != null) {
                try {
                    connection.close();
                } catch (SQLException e) {
                    logger.warning("Error closing connection: " + e.getMessage());
                }
            }
        }
        
        System.out.println("\n=== Program Completed ===");
    }
    
    // GOOD: Specific exception handling
    private static void readConfiguration(String filename) 
            throws IOException {
        // Business logic here
    }
    
    // BAD: Swallowing exception
    private static void badPracticeSwallowException() {
        try {
            riskyOperation();
        } catch (Exception e) {
            // BAD: Exception completely swallowed
            // No logging, no re-throw, no recovery
        }
    }
    
    // GOOD: Proper exception logging
    private static void goodPracticeLogException() {
        try {
            riskyOperation();
        } catch (Exception e) {
            // GOOD: Log the exception
            logger.log(Level.SEVERE, "Operation failed", e);
            
            // Either recover or re-throw
            throw new RuntimeException("Operation failed", e);
        }
    }
    
    // GOOD: Validate before processing (avoid exceptions)
    private static void validateInputBeforeProcessing(String input) {
        if (input == null || input.trim().isEmpty()) {
            // Return error instead of throwing exception
            System.out.println("Invalid input");
            return;
        }
        
        if (input.length() > 100) {
            System.out.println("Input too long");
            return;
        }
        
        // Process valid input
        System.out.println("Processing: " + input);
    }
    
    // GOOD: Preserve cause when wrapping exceptions
    private static void processData() throws ProcessingException {
        try {
            // Some operation that might fail
            parseComplexData();
        } catch (IOException | NumberFormatException e) {
            // Wrap with custom exception, preserving cause
            throw new ProcessingException("Failed to process data", e);
        }
    }
    
    // Helper methods (simulated)
    private static void riskyOperation() {
        throw new RuntimeException("Simulated failure");
    }
    
    private static void processLine(String line) {
        // Process line
    }
    
    private static Connection openDatabaseConnection() throws SQLException {
        return new Connection(); // Simulated
    }
    
    private static void executeQuery(Connection connection) throws SQLException {
        // Execute query
    }
    
    private static void parseComplexData() throws IOException {
        throw new IOException("Simulated parse error");
    }
}

// Custom exception for practice 5
class ProcessingException extends Exception {
    public ProcessingException(String message) {
        super(message);
    }
    
    public ProcessingException(String message, Throwable cause) {
        super(message, cause);
    }
}

// Simulated classes
class Connection implements AutoCloseable {
    public void close() throws SQLException {
        // Close connection
    }
}

class SQLException extends Exception {
    public SQLException(String message) {
        super(message);
    }
}

7. Real-World Exception Handling Examples

Example 1: E-commerce Order Processing

import java.util.*;

public class ECommerceExample {
    
    public static void main(String[] args) {
        System.out.println("=== E-commerce Order Processing ===");
        
        try {
            // Simulate order processing
            Order order = createOrder("ORD001", Arrays.asList(
                new OrderItem("PROD001", 2, 25.99),
                new OrderItem("PROD002", 1, 99.99),
                new OrderItem("PROD003", 0, 49.99)  // Zero quantity
            ));
            
            processOrder(order);
            System.out.println("Order processed successfully!");
            
        } catch (OrderValidationException e) {
            System.out.println("Order validation failed:");
            System.out.println("Order ID: " + e.getOrderId());
            System.out.println("Errors:");
            e.getValidationErrors().forEach((item, error) ->
                System.out.println("  " + item + ": " + error)
            );
            
        } catch (PaymentProcessingException e) {
            System.out.println("Payment processing failed:");
            System.out.println("Order ID: " + e.getOrderId());
            System.out.println("Amount: $" + e.getAmount());
            System.out.println("Error: " + e.getMessage());
            System.out.println("Suggestion: " + e.getRecoverySuggestion());
            
        } catch (InventoryException e) {
            System.out.println("Inventory issue:");
            System.out.println("Product: " + e.getProductId());
            System.out.println("Requested: " + e.getRequestedQuantity());
            System.out.println("Available: " + e.getAvailableQuantity());
            
            if (e.isBackOrderAvailable()) {
                System.out.println("Product available for backorder");
            }
            
        } catch (ShippingException e) {
            System.out.println("Shipping error:");
            System.out.println("Address: " + e.getShippingAddress());
            System.out.println("Error: " + e.getMessage());
            
        } catch (Exception e) {
            System.out.println("Unexpected error: " + e.getMessage());
            logger.severe("Order processing failed", e);
        }
    }
    
    static Order createOrder(String orderId, List items) 
            throws OrderValidationException {
        
        Map errors = new HashMap<>();
        
        if (orderId == null || orderId.trim().isEmpty()) {
            errors.put("orderId", "Order ID cannot be empty");
        }
        
        if (items == null || items.isEmpty()) {
            errors.put("items", "Order must contain at least one item");
        } else {
            for (int i = 0; i < items.size(); i++) {
                OrderItem item = items.get(i);
                if (item.getQuantity() <= 0) {
                    errors.put("item_" + i, 
                        "Quantity must be positive for product: " + 
                        item.getProductId());
                }
                if (item.getUnitPrice() <= 0) {
                    errors.put("item_" + i,
                        "Price must be positive for product: " +
                        item.getProductId());
                }
            }
        }
        
        if (!errors.isEmpty()) {
            throw new OrderValidationException(orderId, errors);
        }
        
        return new Order(orderId, items);
    }
    
    static void processOrder(Order order) 
            throws PaymentProcessingException, 
                   InventoryException, 
                   ShippingException {
        
        // Step 1: Validate payment
        processPayment(order);
        
        // Step 2: Check inventory
        checkInventory(order);
        
        // Step 3: Arrange shipping
        arrangeShipping(order);
        
        // Step 4: Update inventory
        updateInventory(order);
        
        // Step 5: Send notifications
        sendNotifications(order);
    }
    
    static void processPayment(Order order) throws PaymentProcessingException {
        double total = order.calculateTotal();
        
        // Simulate payment processing
        boolean paymentSuccessful = Math.random() > 0.1; // 90% success rate
        
        if (!paymentSuccessful) {
            throw new PaymentProcessingException(
                order.getOrderId(),
                total,
                "Payment declined by bank",
                "Please check your payment details or use a different card"
            );
        }
        
        System.out.println("Payment processed: $" + total);
    }
    
    static void checkInventory(Order order) throws InventoryException {
        for (OrderItem item : order.getItems()) {
            // Simulate inventory check
            int availableStock = getAvailableStock(item.getProductId());
            
            if (availableStock < item.getQuantity()) {
                boolean backOrder = isBackOrderAvailable(item.getProductId());
                
                throw new InventoryException(
                    item.getProductId(),
                    item.getQuantity(),
                    availableStock,
                    backOrder
                );
            }
        }
        
        System.out.println("Inventory check passed");
    }
    
    static void arrangeShipping(Order order) throws ShippingException {
        String address = order.getShippingAddress();
        
        // Simulate shipping validation
        if (address == null || address.trim().isEmpty()) {
            throw new ShippingException(
                address,
                "Shipping address cannot be empty"
            );
        }
        
        if (address.contains("PO Box")) {
            throw new ShippingException(
                address,
                "Cannot ship to PO Box addresses"
            );
        }
        
        System.out.println("Shipping arranged to: " + address);
    }
    
    // Helper methods (simulated)
    static int getAvailableStock(String productId) {
        return (int) (Math.random() * 10); // Random stock
    }
    
    static boolean isBackOrderAvailable(String productId) {
        return Math.random() > 0.5; // 50% chance
    }
    
    static void updateInventory(Order order) {
        System.out.println("Inventory updated");
    }
    
    static void sendNotifications(Order order) {
        System.out.println("Notifications sent");
    }
    
    static void logger(String level, String message, Exception e) {
        System.out.println("[" + level + "] " + message);
        if (e != null) {
            System.out.println("Exception: " + e.getClass().getSimpleName());
        }
    }
}

// ========== MODEL CLASSES ==========

class Order {
    private String orderId;
    private List items;
    private String shippingAddress = "123 Main St";
    
    public Order(String orderId, List items) {
        this.orderId = orderId;
        this.items = items;
    }
    
    public String getOrderId() { return orderId; }
    public List getItems() { return items; }
    public String getShippingAddress() { return shippingAddress; }
    
    public double calculateTotal() {
        return items.stream()
            .mapToDouble(item -> item.getQuantity() * item.getUnitPrice())
            .sum();
    }
}

class OrderItem {
    private String productId;
    private int quantity;
    private double unitPrice;
    
    public OrderItem(String productId, int quantity, double unitPrice) {
        this.productId = productId;
        this.quantity = quantity;
        this.unitPrice = unitPrice;
    }
    
    public String getProductId() { return productId; }
    public int getQuantity() { return quantity; }
    public double getUnitPrice() { return unitPrice; }
}

// ========== CUSTOM EXCEPTIONS ==========

class OrderValidationException extends Exception {
    private String orderId;
    private Map validationErrors;
    
    public OrderValidationException(String orderId, 
                                  Map errors) {
        super("Order validation failed for: " + orderId);
        this.orderId = orderId;
        this.validationErrors = errors;
    }
    
    public String getOrderId() { return orderId; }
    public Map getValidationErrors() { return validationErrors; }
}

class PaymentProcessingException extends Exception {
    private String orderId;
    private double amount;
    private String recoverySuggestion;
    
    public PaymentProcessingException(String orderId, double amount,
                                    String message, String suggestion) {
        super(message);
        this.orderId = orderId;
        this.amount = amount;
        this.recoverySuggestion = suggestion;
    }
    
    public String getOrderId() { return orderId; }
    public double getAmount() { return amount; }
    public String getRecoverySuggestion() { return recoverySuggestion; }
}

class InventoryException extends Exception {
    private String productId;
    private int requestedQuantity;
    private int availableQuantity;
    private boolean backOrderAvailable;
    
    public InventoryException(String productId, int requested,
                            int available, boolean backOrder) {
        super(String.format(
            "Insufficient stock for %s. Requested: %d, Available: %d",
            productId, requested, available
        ));
        this.productId = productId;
        this.requestedQuantity = requested;
        this.availableQuantity = available;
        this.backOrderAvailable = backOrder;
    }
    
    public String getProductId() { return productId; }
    public int getRequestedQuantity() { return requestedQuantity; }
    public int getAvailableQuantity() { return availableQuantity; }
    public boolean isBackOrderAvailable() { return backOrderAvailable; }
}

class ShippingException extends Exception {
    private String shippingAddress;
    
    public ShippingException(String address, String message) {
        super(message);
        this.shippingAddress = address;
    }
    
    public String getShippingAddress() { return shippingAddress; }
}