Exception Handling

Handling Errors Gracefully in Java

← Back to Index

What Are Exceptions?

Imagine you're driving a car and suddenly a tire goes flat. You don't just crash - you pull over, fix the problem, and continue. Exceptions in Java work the same way.

An exception is an unexpected event that occurs during program execution - something that disrupts the normal flow of your code. It could be:

Without exception handling, when an error occurs, your program would crash immediately and stop running. With exception handling, you can catch these errors, deal with them gracefully, and allow your program to continue running or exit cleanly.

Why Exception Handling Matters

Exception handling is crucial because:

  • Prevents crashes - Your program doesn't just stop when something goes wrong
  • Provides user-friendly messages - Instead of cryptic error codes, show helpful messages
  • Allows recovery - Give users a chance to correct errors (like re-entering data)
  • Maintains data integrity - Clean up resources properly even when errors occur
  • Debugging - Helps you understand what went wrong and where

What Happens When an Exception Occurs?

Let's see what happens when code encounters an error:

Example: Division by Zero (Without Exception Handling)

public class CrashExample {
    public static void main(String[] args) {
        System.out.println("Program started");

        int a = 10;
        int b = 0;
        int result = a / b;  // BOOM! Exception thrown here!

        System.out.println("Result: " + result);  // This line NEVER executes
        System.out.println("Program finished");  // This line NEVER executes
    }
}

What Actually Happens:

// Console Output:
Program started
Exception in thread "main" java.lang.ArithmeticException: / by zero
    at CrashExample.main(CrashExample.java:6)

// Program crashes! The last two lines never execute!

Step-by-step breakdown:

  1. Program prints "Program started" ✅
  2. When it tries to divide by zero, Java throws an exception (creates an ArithmeticException object)
  3. Since there's no exception handler, the program crashes immediately
  4. The remaining code never runs ❌
  5. The user sees a scary error message ❌

Try-Catch: Catching Exceptions

Now let's handle that exception properly using a try-catch block!

Example: Handling Division by Zero

public class SafeExample {
    public static void main(String[] args) {
        System.out.println("Program started");

        try {
            // Code that might cause an exception
            int a = 10;
            int b = 0;
            int result = a / b;  // Exception thrown here!

            System.out.println("Result: " + result);  // This line is skipped

        } catch (ArithmeticException e) {
            // Code that runs when exception is caught
            System.out.println("Error: Cannot divide by zero!");
            System.out.println("Please check your numbers and try again.");
        }

        System.out.println("Program finished successfully");  // This DOES execute!
    }
}

What Actually Happens:

// Console Output:
Program started
Error: Cannot divide by zero!
Please check your numbers and try again.
Program finished successfully

// Program runs to completion! ✅

Step-by-step breakdown:

  1. Program enters the try block
  2. When division by zero occurs, Java throws an ArithmeticException
  3. Java immediately jumps out of the try block (skipping remaining try code)
  4. Java looks for a matching catch block
  5. Finds the ArithmeticException catch block and executes that code
  6. After the catch block, execution continues normally
  7. Program completes successfully! ✅
Try-Catch Flow

If NO exception occurs: Code in try block executes completely → catch block is skipped → program continues

If exception occurs: try block stops at the error → matching catch block executes → program continues

Understanding Exception Objects

When an exception is thrown, Java creates an exception object that contains information about the error.

try {
    int[] numbers = {1, 2, 3};
    System.out.println(numbers[10]);  // Out of bounds!

} catch (ArrayIndexOutOfBoundsException e) {
    // 'e' is the exception object - it has useful information!

    // Get the error message
    System.out.println("Message: " + e.getMessage());
    // Output: Message: Index 10 out of bounds for length 3

    // Get the exception type
    System.out.println("Type: " + e.getClass().getName());
    // Output: Type: java.lang.ArrayIndexOutOfBoundsException

    // Print the full stack trace (for debugging)
    e.printStackTrace();
    // Shows exactly where the error occurred in your code
}

Common Exception Methods

Multiple Catch Blocks

Different errors need different handling. You can have multiple catch blocks to handle different types of exceptions:

import java.util.Scanner;

public class MultipleCatchExample {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        try {
            System.out.print("Enter a number: ");
            String input = scanner.nextLine();

            // Might throw NumberFormatException if input is not a number
            int number = Integer.parseInt(input);

            // Might throw ArithmeticException if number is 0
            int result = 100 / number;

            System.out.println("Result: " + result);

        } catch (NumberFormatException e) {
            // Handles invalid number format
            System.out.println("Error: Please enter a valid number!");
            System.out.println("You entered: " + e.getMessage());

        } catch (ArithmeticException e) {
            // Handles division by zero
            System.out.println("Error: Cannot divide by zero!");

        } catch (Exception e) {
            // Catches any other exception
            System.out.println("An unexpected error occurred: " + e.getMessage());
        }

        System.out.println("Program continues...");
    }
}

Example Runs:

// Run 1: User enters "abc"
Enter a number: abc
Error: Please enter a valid number!
You entered: For input string: "abc"
Program continues...

// Run 2: User enters "0"
Enter a number: 0
Error: Cannot divide by zero!
Program continues...

// Run 3: User enters "5"
Enter a number: 5
Result: 20
Program continues...
Important: Catch Block Order

Always put more specific exceptions BEFORE more general ones!

// ✅ CORRECT - Specific first, general last
catch (ArrayIndexOutOfBoundsException e) { }
catch (Exception e) { }

// ❌ WRONG - General first will catch everything!
catch (Exception e) { }  // Catches ALL exceptions
catch (ArrayIndexOutOfBoundsException e) { }  // Never reached!

The Finally Block

The finally block is a special block that ALWAYS executes, whether an exception occurs or not. It's perfect for cleanup code like closing files or database connections.

import java.io.*;

public class FinallyExample {
    public static void main(String[] args) {
        FileWriter file = null;

        try {
            System.out.println("Opening file...");
            file = new FileWriter("data.txt");

            System.out.println("Writing to file...");
            file.write("Hello, World!");

            // Simulate an error
            int x = 10 / 0;  // Exception!

        } catch (IOException e) {
            System.out.println("IO Error: " + e.getMessage());

        } catch (ArithmeticException e) {
            System.out.println("Math Error: " + e.getMessage());

        } finally {
            // This ALWAYS runs - even if there's an exception!
            System.out.println("Closing file...");

            try {
                if (file != null) {
                    file.close();  // Make sure file is closed
                }
            } catch (IOException e) {
                System.out.println("Error closing file");
            }
        }

        System.out.println("Program finished");
    }
}

Output:

Opening file...
Writing to file...
Math Error: / by zero
Closing file...        ← Finally block executed!
Program finished

When does finally execute?

Checked vs Unchecked Exceptions

Java has two categories of exceptions, and understanding the difference is crucial:

1. Checked Exceptions (Compile-Time Exceptions)

These are exceptions that the Java compiler forces you to handle. If you don't handle them, your code won't compile!

Examples: IOException, SQLException, ClassNotFoundException

import java.io.*;

public class CheckedExample {
    // ❌ This will NOT compile!
    public void readFileBad() {
        FileReader file = new FileReader("data.txt");  // Compiler error!
        // Error: Unhandled exception: java.io.FileNotFoundException
    }

    // ✅ Option 1: Handle with try-catch
    public void readFileGood1() {
        try {
            FileReader file = new FileReader("data.txt");
            // Use the file...
        } catch (FileNotFoundException e) {
            System.out.println("File not found: " + e.getMessage());
        }
    }

    // ✅ Option 2: Declare with throws (pass it to caller)
    public void readFileGood2() throws FileNotFoundException {
        FileReader file = new FileReader("data.txt");
        // Now the caller must handle this exception
    }
}

2. Unchecked Exceptions (Runtime Exceptions)

These are exceptions that occur during runtime. The compiler doesn't force you to handle them, but you can if you want to.

Examples: NullPointerException, ArithmeticException, ArrayIndexOutOfBoundsException

public class UncheckedExample {
    // This compiles fine - no try-catch required
    public void divide(int a, int b) {
        int result = a / b;  // Might throw ArithmeticException
        System.out.println(result);
    }

    // But you CAN catch them if you want to handle them
    public void divideSafely(int a, int b) {
        try {
            int result = a / b;
            System.out.println(result);
        } catch (ArithmeticException e) {
            System.out.println("Cannot divide by zero!");
        }
    }
}
When to Use Each

Checked Exceptions: For problems you can potentially recover from (file not found, network error)

Unchecked Exceptions: For programming errors that shouldn't happen (null pointer, invalid index)

Throwing Your Own Exceptions

You can create and throw exceptions to signal errors in your own code:

public class BankAccount {
    private double balance;

    public void withdraw(double amount) {
        // Check for negative amount
        if (amount < 0) {
            // Throw an exception!
            throw new IllegalArgumentException(
                "Amount cannot be negative: " + amount
            );
        }

        // Check for insufficient funds
        if (amount > balance) {
            throw new IllegalStateException(
                "Insufficient funds. Balance: " + balance + ", Requested: " + amount
            );
        }

        // All checks passed - perform withdrawal
        balance -= amount;
        System.out.println("Withdrew: $" + amount);
    }
}

// Usage:
BankAccount account = new BankAccount();
account.balance = 100;

try {
    account.withdraw(-50);  // Throws IllegalArgumentException
} catch (IllegalArgumentException e) {
    System.out.println("Error: " + e.getMessage());
    // Output: Error: Amount cannot be negative: -50
}

Try-With-Resources (Automatic Cleanup)

Java 7 introduced a cleaner way to handle resources that need to be closed:

// Old way (lots of code!)
FileReader file = null;
try {
    file = new FileReader("data.txt");
    // Use file...
} catch (IOException e) {
    e.printStackTrace();
} finally {
    if (file != null) {
        try {
            file.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

// New way (automatic cleanup!) ✅
try (FileReader file = new FileReader("data.txt")) {
    // Use file...
    // File is automatically closed when try block ends!
} catch (IOException e) {
    e.printStackTrace();
}

Common Exceptions You'll Encounter

Exception When It Occurs Example
NullPointerException Using null reference String s = null; s.length();
ArrayIndexOutOfBoundsException Invalid array index int[] arr = {1,2}; arr[5];
ArithmeticException Math error (division by zero) int x = 10/0;
NumberFormatException Invalid number format Integer.parseInt("abc");
IOException Input/Output error Reading non-existent file
FileNotFoundException File doesn't exist new FileReader("missing.txt");

Best Practices

Exception Handling Best Practices
  1. Be Specific - Catch specific exceptions, not just Exception
  2. Don't Ignore - Never have empty catch blocks
  3. Log Errors - Always log exceptions for debugging
  4. User-Friendly Messages - Show helpful messages to users
  5. Clean Up Resources - Use finally or try-with-resources
  6. Don't Use for Control Flow - Exceptions are for exceptional cases
// ❌ BAD - Empty catch (hides errors!)
try {
    riskyOperation();
} catch (Exception e) {
    // Silent failure - bad!
}

// ✅ GOOD - Handle properly
try {
    riskyOperation();
} catch (IOException e) {
    logger.error("Failed to perform operation", e);
    System.out.println("An error occurred. Please try again.");
}