Exception Handling

try-catch, throws, and custom exceptions

1. Exception Overview

Exceptions are runtime events that disrupt normal program flow. Proper exception handling ensures robust, maintainable applications.

Checked Exceptions

Must be handled at compile time

IOException, SQLException

Unchecked Exceptions

Runtime exceptions, optional handling

NullPointerException, ArrayIndexOutOfBoundsException

Errors

Serious problems, typically not recoverable

OutOfMemoryError, StackOverflowError

Exception Flow

1

Exception Occurs

Runtime error in code execution

2

Exception Object Created

JVM creates exception instance

3

Handler Search

JVM looks for appropriate catch block

4

Handle or Terminate

Execute handler or terminate program

2. Exception Hierarchy

Java’s exception hierarchy is built on the Throwable class, branching into Errors and Exceptions.

Exception Examples

// Checked Exception - Must handle
try {
    FileReader file = new FileReader("nonexistent.txt");
} catch (IOException e) {
    System.out.println("File not found: " + e.getMessage());
}

// Unchecked Exception - Optional handling
String str = null;
try {
    int length = str.length(); // NullPointerException
} catch (NullPointerException e) {
    System.out.println("Null reference error");
}

3. Try-Catch Blocks

try {}

Code that might throw exception

catch (ExceptionType e) {}

Handle specific exception

finally {}

Always executes (optional)

Basic Try-Catch

Basic Try-Catch

try {
    int result = 10 / 0; // ArithmeticException
} catch (ArithmeticException e) {
    System.out.println("Cannot divide by zero!");
}

Multiple Catch Blocks

Multiple Catch Blocks

try {
    int[] arr = {1, 2, 3};
    System.out.println(arr[5]); // ArrayIndexOutOfBoundsException
    int result = 10 / 0; // ArithmeticException
} catch (ArrayIndexOutOfBoundsException e) {
    System.out.println("Array index out of bounds");
} catch (ArithmeticException e) {
    System.out.println("Arithmetic error");
} catch (Exception e) {
    System.out.println("General exception: " + e.getMessage());
}

Multi-Catch (Java 7+)

Multi-Catch

try {
    // Some risky operations
    performOperations();
} catch (IOException | SQLException e) {
    System.out.println("IO or Database error: " + e.getMessage());
    logger.error("Operation failed", e);
}

Try-Catch-Finally

Try-Catch-Finally

FileReader file = null;
try {
    file = new FileReader("data.txt");
    // Process file
} catch (IOException e) {
    System.out.println("File error: " + e.getMessage());
} finally {
    // Always executes
    if (file != null) {
        try {
            file.close();
        } catch (IOException e) {
            System.out.println("Error closing file");
        }
    }
}

Try-With-Resources (Java 7+)

Try-With-Resources

// Automatically closes resources
try (FileReader file = new FileReader("data.txt");
     BufferedReader buffer = new BufferedReader(file)) {

    String line = buffer.readLine();
    System.out.println(line);

} catch (IOException e) {
    System.out.println("File processing error: " + e.getMessage());
}
// Resources automatically closed here

Execution Order

1

Try Block Executes

If no exception → Skip catch blocks

2

Exception Occurs

Jump to appropriate catch block

3

Finally Block

Always executes (if present)

4. Throws & Throw

throws

  • Used in: Method signature
  • Purpose: Declares potential exceptions
  • Action: Passes responsibility to caller

throw

  • Used in: Method body
  • Purpose: Actually throws an exception
  • Action: Creates and throws exception object

Using ‘throws’ Keyword

Using throws

// Method declares it might throw IOException
public void readFile(String filename) throws IOException {
    FileReader file = new FileReader(filename);
    // File operations...
}

// Method with multiple exceptions
public void databaseOperation() throws SQLException, ClassNotFoundException {
    // Database operations...
}

// Caller must handle the exception
public void processData() {
    try {
        readFile("data.txt");
    } catch (IOException e) {
        System.out.println("Failed to read file: " + e.getMessage());
    }
}

Using ‘throw’ Keyword

Using throw

// Explicitly throwing an exception
public void validateAge(int age) {
    if (age 0) {
        throw new IllegalArgumentException("Age cannot be negative");
    }
    if (age > 150) {
        throw new IllegalArgumentException("Age seems unrealistic");
    }
}

// Re-throwing caught exception
public void processFile(String filename) throws IOException {
    try {
        // File operations
        Files.readAllLines(Paths.get(filename));
    } catch (IOException e) {
        // Log the error
        logger.error("File processing failed", e);
        throw e; // Re-throw to caller
    }
}

5. Custom Exceptions

Custom exceptions provide specific error handling for your application domain, making code more readable and maintainable.

Custom Checked Exception

Custom Checked Exception

// Extends Exception for checked exception
public class InsufficientFundsException extends Exception {
    private double amount;

    public InsufficientFundsException(double amount) {
        super("Insufficient funds: $" + amount + " required");
        this.amount = amount;
    }

    public double getAmount() {
        return amount;
    }
}

Custom Unchecked Exception

Custom Unchecked Exception

// Extends RuntimeException for unchecked exception
public class InvalidUserException extends RuntimeException {
    private String userId;

    public InvalidUserException(String userId, String message) {
        super(message);
        this.userId = userId;
    }

    public String getUserId() {
        return userId;
    }
}

Exception Design Guidelines

  • Descriptive Names: Use clear, specific names that describe the error condition
  • Meaningful Messages: Provide context about what went wrong and how to fix it
  • Additional Data: Include relevant information for error handling (error codes, timestamps)
  • Proper Hierarchy: Extend appropriate exception class

Complete Custom Exception Example

Banking System Example

// Custom exception for banking operations
public class BankingException extends Exception {
    private String accountNumber;
    private String errorCode;

    public BankingException(String accountNumber, String errorCode, String message) {
        super(message);
        this.accountNumber = accountNumber;
        this.errorCode = errorCode;
    }

    // Getters
    public String getAccountNumber() { return accountNumber; }
    public String getErrorCode() { return errorCode; }
}

// Usage in banking service
public class BankAccount {
    private double balance;
    private String accountNumber;

    public void withdraw(double amount) throws BankingException {
        if (amount 0) {
            throw new BankingException(accountNumber, "INVALID_AMOUNT",
                "Withdrawal amount must be positive");
        }

        if (amount > balance) {
            throw new BankingException(accountNumber, "INSUFFICIENT_FUNDS",
                "Insufficient balance. Available: $" + balance +
                ", Requested: $" + amount);
        }

        balance -= amount;
    }
}

// Exception handling
try {
    account.withdraw(1000);
} catch (BankingException e) {
    System.out.println("Banking Error [" + e.getErrorCode() + "]: " + e.getMessage());
    System.out.println("Account: " + e.getAccountNumber());
}

Custom Exception Hierarchy

Exception Hierarchy

// Base application exception
public class ApplicationException extends Exception {
    protected String errorCode;
    protected long timestamp;

    public ApplicationException(String errorCode, String message) {
        super(message);
        this.errorCode = errorCode;
        this.timestamp = System.currentTimeMillis();
    }
}

// Specific business exceptions
public class UserNotFoundException extends ApplicationException {
    public UserNotFoundException(String userId) {
        super("USER_NOT_FOUND", "User not found: " + userId);
    }
}

public class ValidationException extends ApplicationException {
    public ValidationException(String field, String message) {
        super("VALIDATION_ERROR", "Invalid " + field + ": " + message);
    }
}

6. Best Practices

Good vs Bad Examples

Bad: Generic Exception Catching

try {
    // Some operations
    performDatabaseOperation();
    processFile();
} catch (Exception e) {
    // Too generic!
    System.out.println("Something went wrong");
}

Good: Specific Exception Handling

try {
    performDatabaseOperation();
    processFile();
} catch (SQLException e) {
    logger.error("Database operation failed", e);
    // Handle database-specific recovery
} catch (IOException e) {
    logger.error("File processing failed", e);
    // Handle file-specific recovery
}

Bad: Empty Catch Block

try {
    int value = Integer.parseInt(userInput);
} catch (NumberFormatException e) {
    // Silent failure - very bad!
}

Good: Proper Error Handling

try {
    int value = Integer.parseInt(userInput);
} catch (NumberFormatException e) {
    logger.warn("Invalid number format: " + userInput, e);
    showUserMessage("Please enter a valid number");
    return getDefaultValue();
}

Exception Logging Levels

  • ERROR: System errors, exceptions that affect functionality
  • WARN: Recoverable issues, degraded functionality
  • INFO: Expected exceptions, business logic flow
  • DEBUG: Detailed troubleshooting information

Testing Exception Handling

JUnit Exception Testing

// JUnit 5 - Test expected exception
@Test
void testInvalidAge() {
    IllegalArgumentException exception = assertThrows(
        IllegalArgumentException.class,
        () -> validateAge(-5)
    );

    assertEquals("Age cannot be negative", exception.getMessage());
}

// Test custom exception
@Test
void testInsufficientFunds() {
    BankAccount account = new BankAccount(100);

    BankingException exception = assertThrows(
        BankingException.class,
        () -> account.withdraw(200)
    );

    assertEquals("INSUFFICIENT_FUNDS", exception.getErrorCode());
}

Documenting Exceptions

JavaDoc for Exceptions

/**
 * Transfers money between accounts
 *
 * @param fromAccount source account
 * @param toAccount destination account
 * @param amount transfer amount
 * @throws BankingException if insufficient funds or account issues
 * @throws IllegalArgumentException if amount is negative or zero
 * @throws AccountNotFoundException if either account doesn't exist
 */
public void transfer(String fromAccount, String toAccount, double amount)
    throws BankingException, IllegalArgumentException, AccountNotFoundException {
    // Implementation
}