Java Exception Handling

What is an Exception?

In Java, an exception is an unexpected event or error that occurs during the execution of a program, disrupting its normal flow. Exceptions arise when the program encounters a condition it cannot handle, such as invalid user input, attempting to read a non-existent file, or dividing a number by zero.

When an exception occurs, Java provides a mechanism to detect, handle, and recover from these errors gracefully, rather than letting the program crash unexpectedly.

Example of an Exception:

int result = 10 / 0; // Causes ArithmeticException: division by zero

If this exception is not handled, the program will terminate abruptly with an error message.

Errors vs. Exceptions

Java delineates errors and exceptions based on severity and recoverability. Errors are critical, system-level malfunctions beyond a programmer's control, often caused by resource depletion or JVM failures, such as OutOfMemoryError when the JVM exhausts its memory pool or StackOverflowError due to unchecked recursion depth. These issues are typically irrecoverable and are not meant to be caught or managed by application logic. In contrast, exceptions signify recoverable disruptions stemming from invalid operations, user input errors, or flawed application logic. They are designed to be anticipated and handled gracefully, allowing the program to recover and proceed. Java enforces handling for checked exceptions like IOException, which arises during file operations, ensuring robust code that can cope with external failures. Unchecked exceptions, such as NullPointerException and ArithmeticException, stem from logical errors within the code and do not require mandatory handling. Proper management of exceptions, whether checked or unchecked, enhances the program's resilience and ensures graceful degradation rather than abrupt termination.

Errors

Example:

public class StackOverflowDemo {
    public static void infiniteRecursion() {
        infiniteRecursion(); // Causes StackOverflowError
    }

    public static void main(String[] args) {
        infiniteRecursion();
    }
}

Exceptions

Example:

public class ExceptionDemo {
    public static void main(String[] args) {
        String str = null;
        System.out.println(str.length()); // Causes NullPointerException
    }
}

Core Constructs

1. try Block

  • The try block contains code that may throw an exception during execution. It acts as a safety net, allowing you to enclose risky operations.

Example:

try {
    int result = 10 / 0; // May cause ArithmeticException
}

2. catch Block

  • The catch block handles exceptions thrown by the try block. It specifies what type of exception it can catch and provides a mechanism for recovery or logging.

Example:

try {
    int result = 10 / 0;
} catch (ArithmeticException e) {
    System.out.println("Division by zero is not allowed.");
}

Catching Superclass Exceptions

  • When a superclass exception (e.g., Exception) is caught before a subclass exception (e.g., ArithmeticException), the superclass block captures all exceptions of that type and its subclasses, preventing the subclass-specific block from executing.

Example:

try {
    int result = 10 / 0;
} catch (Exception e) {
    System.out.println("Caught by superclass Exception.");
} catch (ArithmeticException e) {
    // This block will never be reached
    System.out.println("Caught ArithmeticException.");
}

Explanation:
In this example, Exception is the superclass of ArithmeticException. Since the Exception block appears first, it captures all exceptions derived from Exception, including ArithmeticException. This prevents the more specific ArithmeticException block from executing.

3. finally Block

  • The finally block contains code that always executes regardless of whether an exception is thrown or not. It is typically used for cleanup operations, such as closing files or releasing resources.

Example:

try {
    FileReader reader = new FileReader("file.txt");
} catch (FileNotFoundException e) {
    System.out.println("File not found.");
} finally {
    System.out.println("Cleanup complete.");
}

Explanation:

  • The finally block runs whether or not the FileNotFoundException occurs, ensuring that cleanup tasks are completed.

4. throw Keyword

  • The throw keyword is used to explicitly raise an exception in your code. It can be used to signal that a method encountered an invalid condition.

Example:

public void validateAge(int age) {
    if (age < 18) {
        throw new IllegalArgumentException("Age must be 18 or older.");
    }
}

Explanation:

  • In this example, if age is less than 18, an IllegalArgumentException is explicitly thrown, stopping the normal execution of the method.

5. throws Keyword

  • The throws keyword is used in a method signature to declare exceptions that the method might throw. This signals to the caller that they must handle or further declare the exception.

Example:

public void readFile() throws IOException {
    FileReader reader = new FileReader("file.txt");
}

Explanation:

  • The readFile method declares that it might throw an IOException. The calling method must either handle this exception with a try-catch block or declare it using throws.

Putting It All Together

Here’s a complete example demonstrating all these constructs:

import java.io.FileReader;
import java.io.FileNotFoundException;
import java.io.IOException;

public class ExceptionHandlingDemo {
    public static void readFile(String filename) throws IOException {
        try {
            FileReader reader = new FileReader(filename);
            System.out.println("File opened successfully.");
        } catch (FileNotFoundException e) {
            System.out.println("File not found: " + e.getMessage());
        } finally {
            System.out.println("Execution of finally block: Closing resources.");
        }
    }

    public static void validateAge(int age) {
        if (age < 18) {
            throw new IllegalArgumentException("Age must be 18 or older.");
        }
        System.out.println("Age is valid.");
    }

    public static void main(String[] args) {
        try {
            readFile("example.txt");
            validateAge(16);
        } catch (IOException e) {
            System.out.println("IOException occurred: " + e.getMessage());
        } catch (IllegalArgumentException e) {
            System.out.println("Validation Error: " + e.getMessage());
        }
    }
}

Explanation:

  1. readFile: Demonstrates the use of try, catch, finally, and throws.

  2. validateAge: Shows how to use throw to explicitly raise an exception.

  3. main: Calls both methods and handles exceptions using multiple catch blocks.

Key Takeaways

  • try: Contains code that may throw exceptions.

  • catch: Handles exceptions thrown in the try block.

  • Superclass Catching: Placing a superclass exception before a subclass in catch blocks will prevent the subclass handler from executing.

  • finally: Executes cleanup code regardless of exceptions.

  • throw: Explicitly raises an exception.

  • throws: Declares exceptions that a method might throw.

Java Exception Class Hierarchy

Java’s exception-handling system is structured around the Throwable class, which serves as the root of all errors and exceptions. The hierarchy enables Java programs to represent error conditions as objects and manage them through structured exception handling mechanisms.

The Throwable class branches into two primary subclasses: Error and Exception. The Exception class is further divided into checked exceptions and unchecked exceptions (runtime exceptions). This classification helps distinguish between severe system-level failures and recoverable application-level issues.

Hierarchy Overview

Throwable
├── Error (Unchecked)
│   └── Examples: OutOfMemoryError, StackOverflowError
└── Exception
    ├── Checked Exceptions
    │   └── Examples: IOException, SQLException
    └── RuntimeException (Unchecked)
        └── Examples: NullPointerException, ArithmeticException

1. Error Class

  • Definition:
    Represents severe system-level issues that are typically beyond the control of the application.

  • Nature:
    Errors indicate critical failures, such as hardware malfunctions or JVM-level problems. These are unchecked because they are not meant to be anticipated or handled by the application code.

  • Characteristics:

    • Recovery is generally not possible or advisable.

    • Not meant to be caught or handled by regular application logic.

  • Common Examples:

    • OutOfMemoryError: Indicates that the JVM has run out of memory.

    • StackOverflowError: Occurs when the call stack limit is exceeded due to deep recursion.

2. Exception Class

  • Definition:
    Represents application-level issues that a program can anticipate, handle, and recover from.

  • Nature:
    Exceptions are divided into checked exceptions and unchecked exceptions based on whether the compiler enforces handling them.

Checked Exceptions

  • Definition:
    Exceptions that the compiler requires the programmer to handle explicitly, either by catching them or declaring them with the throws keyword.

  • Characteristics:

    • Represent anticipated issues that are outside the program's direct control.

    • Examples include file I/O errors or database connection failures.

  • Common Examples:

    • IOException: Occurs during input/output operations (e.g., reading a missing file).

    • SQLException: Indicates issues when accessing a database.

Unchecked Exceptions (Runtime Exceptions)

  • Definition:
    Exceptions that the compiler does not require to be explicitly handled.

  • Characteristics:

    • Typically caused by programming logic errors.

    • These exceptions can be caught if desired, but handling is not mandatory.

  • Common Examples:

    • NullPointerException: Occurs when accessing a null reference.

    • ArithmeticException: Occurs when performing illegal arithmetic operations, such as division by zero.

Checked vs. Unchecked Exceptions in Java

Understanding the distinction between checked and unchecked exceptions is crucial for writing robust and maintainable Java code. Java's exception-handling mechanism categorizes exceptions based on how the compiler enforces their handling.

Checked Exceptions

Definition

Checked exceptions are exceptions that the compiler requires you to handle explicitly. These exceptions represent scenarios where failures are anticipated, and handling them is necessary to prevent unexpected program termination.

Examples

  • IOException: Occurs during input/output operations (e.g., file not found).

  • SQLException: Indicates issues with database access.

  • ClassNotFoundException: Raised when a class is not found at runtime.

When They Occur

Checked exceptions typically occur in operations involving external resources, such as:

  • File I/O: Reading from or writing to files.

  • Database Operations: Connecting to or querying a database.

  • Network Operations: Communicating over the network.

How to Handle Checked Exceptions

  1. Declare with throws in the Method Signature
    If a method might throw a checked exception, you can declare it using the throws keyword.

    Example:

     public void readFile() throws IOException {
         FileReader reader = new FileReader("file.txt");
     }
    
  2. Catch with try-catch Block
    You can handle checked exceptions directly within the method using a try-catch block.

    Example:

     public void readFile() {
         try {
             FileReader reader = new FileReader("file.txt");
         } catch (IOException e) {
             System.out.println("Error reading file: " + e.getMessage());
         }
     }
    
  3. Re-throwing Checked Exceptions
    If a method does not handle a checked exception, it can re-throw it to the calling method.

    Example:

     public void processFile() throws IOException {
         readFile();
     }
    

Key Rule

You must either:

  • Catch the checked exception with a try-catch block, or

  • Declare it in the method signature using the throws keyword.

Unchecked Exceptions

Definition

Unchecked exceptions are exceptions that the compiler does not require you to handle. These exceptions typically arise from programming logic errors that could be avoided with proper code checks.

Examples

  • NullPointerException: Accessing an object reference that is null.

  • ArithmeticException: Division by zero.

  • ArrayIndexOutOfBoundsException: Accessing an invalid index in an array.

When They Occur

Unchecked exceptions usually stem from:

  • Programming Mistakes: Incorrect logic or assumptions.

  • Invalid Operations: Operations performed on invalid data structures (e.g., accessing beyond array bounds).

How to Handle Unchecked Exceptions

  1. Optional Handling with try-catch
    You can handle unchecked exceptions with a try-catch block, but it is not mandatory.

    Example:

     public void divide(int a, int b) {
         try {
             int result = a / b;
         } catch (ArithmeticException e) {
             System.out.println("Division by zero is not allowed.");
         }
     }
    
  2. No Need to Declare
    You do not need to declare unchecked exceptions in the method signature with throws.

    Example:

     public void unsafeMethod() {
         int[] nums = {1, 2, 3};
         System.out.println(nums[3]); // ArrayIndexOutOfBoundsException
     }
    

Key Rule

You are not required to catch or declare unchecked exceptions, but handling them can improve program robustness and user experience.

Summary of Checked vs. Unchecked Exceptions

AspectChecked ExceptionsUnchecked Exceptions
Compiler EnforcementMust be caught or declared with throwsOptional handling; no need to declare
Typical CausesExternal issues (e.g., file, database errors)Programming logic errors (e.g., null refs)
Common ExamplesIOException, SQLExceptionNullPointerException, ArithmeticException
HandlingMandatory try-catch or throws declarationOptional try-catch

Best Practices for Exception Handling in Java

Effective exception handling is essential for writing robust, maintainable, and efficient Java programs. Here are detailed best practices to help you manage exceptions properly, ensuring that your code remains resilient and easy to debug.

1. Handle Checked Exceptions Appropriately Using try-catch or throws

Checked exceptions indicate conditions that a well-designed application should anticipate and recover from. They typically arise from operations involving external resources, such as file I/O, database access, or network communication.

Best Practices for Handling Checked Exceptions

  1. Use try-catch When Recovery is Possible:
    If you can handle the exception within the method, use a try-catch block to provide meaningful recovery or fallback behavior.

    Example:

     public void readFile(String filePath) {
         try {
             FileReader reader = new FileReader(filePath);
             System.out.println("File read successfully.");
         } catch (FileNotFoundException e) {
             System.out.println("File not found: " + filePath);
         }
     }
    
  2. Use throws When You Want the Caller to Handle It:
    If the method cannot handle the exception meaningfully, declare it with throws and let the caller decide how to manage it.

    Example:

     public void loadFile(String filePath) throws IOException {
         FileReader reader = new FileReader(filePath);
     }
    
  3. Avoid Empty catch Blocks:
    Catching exceptions without any handling logic can mask problems and make debugging difficult.

    Bad Practice:

     try {
         FileReader reader = new FileReader("data.txt");
     } catch (IOException e) {
         // Empty catch block - bad practice
     }
    

2. Avoid Catching Generic Exceptions (Exception or Throwable)

Catching broad exceptions like Exception or Throwable can obscure the specific cause of the problem and lead to unintended behavior.

Why You Should Avoid This

  • Loss of Specificity: You might catch exceptions you didn't intend to handle.

  • Hides Errors: Critical errors (e.g., OutOfMemoryError) may be caught unintentionally.

  • Harder to Debug: You lose clarity on what went wrong.

Bad Practice:

try {
    riskyOperation();
} catch (Exception e) {
    System.out.println("An error occurred."); // Too generic
}

Better Approach

Catch specific exceptions to handle different failure scenarios appropriately.

Example:

try {
    riskyOperation();
} catch (FileNotFoundException e) {
    System.out.println("File not found: " + e.getMessage());
} catch (IOException e) {
    System.out.println("I/O error occurred: " + e.getMessage());
}

3. Catch Specific Exceptions First Before Superclasses

When catching multiple exceptions, always catch the most specific exceptions first before catching their superclasses. Catching a superclass exception (e.g., Exception) before a subclass exception (e.g., FileNotFoundException) will prevent the subclass handler from being executed.

Example of Incorrect Order

try {
    FileReader reader = new FileReader("data.txt");
} catch (Exception e) {
    System.out.println("General exception caught.");
} catch (FileNotFoundException e) {
    // This block will never be reached
    System.out.println("File not found.");
}

Correct Order

try {
    FileReader reader = new FileReader("data.txt");
} catch (FileNotFoundException e) {
    System.out.println("File not found.");
} catch (Exception e) {
    System.out.println("General exception caught.");
}

4. Use Defensive Coding to Minimize Unchecked Exceptions

Unchecked exceptions (e.g., NullPointerException, ArrayIndexOutOfBoundsException) often stem from programming logic errors. Defensive coding helps prevent these exceptions by validating inputs and assumptions before performing operations.

Defensive Coding Techniques

  1. Check for null References:

     public void printLength(String str) {
         if (str != null) {
             System.out.println("Length: " + str.length());
         } else {
             System.out.println("String is null.");
         }
     }
    
  2. Validate Array Indexes:

     public void accessArrayElement(int[] array, int index) {
         if (index >= 0 && index < array.length) {
             System.out.println("Element: " + array[index]);
         } else {
             System.out.println("Invalid index.");
         }
     }
    
  3. Check Divisor for Zero:

     public void divide(int a, int b) {
         if (b != 0) {
             System.out.println("Result: " + (a / b));
         } else {
             System.out.println("Cannot divide by zero.");
         }
     }
    

5. Clean Up Resources Using finally or Try-With-Resources

When dealing with resources such as files, sockets, or database connections, it’s important to release them properly to avoid resource leaks. Use the finally block or the try-with-resources statement for automatic resource management.

Using finally Block

The finally block ensures that cleanup code is executed regardless of whether an exception occurs.

Example:

FileReader reader = null;
try {
    reader = new FileReader("data.txt");
    // Read file
} catch (IOException e) {
    System.out.println("Error reading file: " + e.getMessage());
} finally {
    if (reader != null) {
        try {
            reader.close();
        } catch (IOException e) {
            System.out.println("Error closing file: " + e.getMessage());
        }
    }
}

Using Try-With-Resources

Introduced in Java 7, the try-with-resources statement automatically closes resources that implement the AutoCloseable interface.

Example:

try (FileReader reader = new FileReader("data.txt")) {
    // Read file
    System.out.println("File read successfully.");
} catch (IOException e) {
    System.out.println("Error reading file: " + e.getMessage());
}

Advantages:

  • No need for an explicit finally block.

  • Simplifies code and reduces boilerplate.

Code Examples for Reinforcement

1. StackOverflowError

Explanation: A StackOverflowError occurs when a method calls itself recursively without a base case or when the recursion depth exceeds the stack size. This is an Error, not an Exception, and does not need to be caught or declared. Errors typically indicate serious problems that the application cannot handle.

Code Example:

public class StackOverflowDemo {
    public static void main(String[] args) {
        causeStackOverflow();
    }

    public static void causeStackOverflow() {
        causeStackOverflow(); // Infinite recursion
    }
}

Output:

Exception in thread "main" java.lang.StackOverflowError
    at StackOverflowDemo.causeStackOverflow(StackOverflowDemo.java:6)
    at StackOverflowDemo.causeStackOverflow(StackOverflowDemo.java:6)
    ...

Explanation: The program terminates with a StackOverflowError and prints a stack trace. Since it is an Error, it is unexpected and unrecoverable.

2. RuntimeExceptions

Explanation: These are unchecked exceptions, meaning they do not need to be declared or caught. They indicate programming errors that can often be prevented with defensive coding. Examples include NullPointerException, ArrayIndexOutOfBoundsException, IllegalArgumentException, and ArithmeticException.

Code Examples:

NullPointerException

public class NullPointerDemo {
    public static void main(String[] args) {
        String str = null;
        System.out.println(str.length()); // Accessing a null reference
    }
}

Fix:

public class NullPointerFixed {
    public static void main(String[] args) {
        String str = null;
        if (str != null) {
            System.out.println(str.length());
        } else {
            System.out.println("String is null!");
        }
    }
}

ArrayIndexOutOfBoundsException

public class ArrayIndexDemo {
    public static void main(String[] args) {
        int[] arr = {1, 2, 3};
        System.out.println(arr[5]); // Accessing an invalid index
    }
}

Fix:

public class ArrayIndexFixed {
    public static void main(String[] args) {
        int[] arr = {1, 2, 3};
        int index = 5;
        if (index >= 0 && index < arr.length) {
            System.out.println(arr[index]);
        } else {
            System.out.println("Index out of bounds!");
        }
    }
}

IllegalArgumentException

public class IllegalArgumentDemo {
    public static void setAge(int age) {
        if (age < 0) {
            throw new IllegalArgumentException("Age cannot be negative");
        }
        System.out.println("Age set to: " + age);
    }

    public static void main(String[] args) {
        setAge(-5); // Invalid argument
    }
}

Fix: Defensive coding already prevents the issue by throwing an exception for invalid input.

ArithmeticException

public class ArithmeticDemo {
    public static void main(String[] args) {
        int result = 10 / 0; // Division by zero
        System.out.println("Result: " + result);
    }
}

Fix:

public class ArithmeticFixed {
    public static void main(String[] args) {
        int divisor = 0;
        if (divisor != 0) {
            System.out.println("Result: " + (10 / divisor));
        } else {
            System.out.println("Cannot divide by zero!");
        }
    }
}

3. Custom Checked Exception

Explanation: Create a custom checked exception by extending Exception.

Code Example:

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

4. Method Throwing Multiple Checked Exceptions

Code Example:

import java.io.IOException;

public class MultipleExceptionsDemo {
    public void riskyMethod() throws IOException, CustomCheckedException {
        if (Math.random() > 0.5) {
            throw new IOException("IO problem occurred");
        } else {
            throw new CustomCheckedException("Custom problem occurred");
        }
    }
}

5. Handling Multiple Checked Exceptions

Code Example:

Catch one, declare one:

public void handleOne() throws IOException {
    try {
        new MultipleExceptionsDemo().riskyMethod();
    } catch (CustomCheckedException e) {
        System.out.println("Handled CustomCheckedException: " + e.getMessage());
    }
}

Catch both, no declaration:

public void handleBoth() {
    try {
        new MultipleExceptionsDemo().riskyMethod();
    } catch (IOException | CustomCheckedException e) {
        System.out.println("Handled exception: " + e.getMessage());
    }
}

Catch both, rethrow one:

public void handleAndRethrow() throws IOException {
    try {
        new MultipleExceptionsDemo().riskyMethod();
    } catch (CustomCheckedException e) {
        System.out.println("Handled CustomCheckedException: " + e.getMessage());
    } catch (IOException e) {
        System.out.println("Caught IOException: " + e.getMessage());
        throw e;
    }
}

Catch both, rethrow new exception:

public void handleAndThrowNew() throws Exception {
    try {
        new MultipleExceptionsDemo().riskyMethod();
    } catch (IOException | CustomCheckedException e) {
        throw new Exception("Wrapped Exception: " + e.getMessage());
    }
}

6. Nested Try-Catch

Code Example:

public class NestedTryCatch {
    public static void main(String[] args) {
        try {
            try {
                throw new IOException("Inner exception");
            } catch (IOException e) {
                System.out.println("Inner catch: " + e.getMessage());
                throw e; // Rethrow
            }
        } catch (IOException e) {
            System.out.println("Outer catch: " + e.getMessage());
        }
    }
}

7. Custom Exceptions with Inheritance

Code Example:

public class ParentException extends Exception {}
public class ChildException extends ParentException {}

public class ExceptionOrderDemo {
    public static void main(String[] args) {
        try {
            throw new ChildException();
        } catch (ParentException e) {
            System.out.println("Caught ParentException");
        } catch (ChildException e) { // Unreachable
            System.out.println("Caught ChildException");
        }
    }
}

Explanation: Catching ParentException first makes the ChildException catch block unreachable, resulting in a compilation error. Always catch specific exceptions before general ones.

8. Code in finally is Always Executed

Explanation: The finally block is executed no matter what happens in the try block—whether an exception is thrown, caught, or no exception occurs. This is typically used for cleanup operations like closing resources.

Code Example:

public class FinallyAlwaysExecutes {
    public static void main(String[] args) {
        try {
            System.out.println("In try block");
            int result = 10 / 0; // This will throw ArithmeticException
        } catch (ArithmeticException e) {
            System.out.println("Exception caught: " + e.getMessage());
        } finally {
            System.out.println("Finally block executed");
        }
    }
}

Output:

In try block
Exception caught: / by zero
Finally block executed

Explanation: Even though an exception is thrown and caught, the finally block is still executed.

9. finally Executes Even Without a catch

Explanation: The finally block will always execute even if there is no catch block. If an exception is thrown but not caught, the finally block is executed before the exception propagates further.

Code Example:

public class FinallyWithoutCatch {
    public static void main(String[] args) {
        try {
            System.out.println("In try block");
            int result = 10 / 0; // This will throw ArithmeticException
        } finally {
            System.out.println("Finally block executed");
        }
        // No catch block here
    }
}

Output:

In try block
Finally block executed
Exception in thread "main" java.lang.ArithmeticException: / by zero
    at FinallyWithoutCatch.main(FinallyWithoutCatch.java:5)

Explanation: The finally block is executed, and then the uncaught exception terminates the program.

5
Subscribe to my newsletter

Read articles from Jyotiprakash Mishra directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Jyotiprakash Mishra
Jyotiprakash Mishra

I am Jyotiprakash, a deeply driven computer systems engineer, software developer, teacher, and philosopher. With a decade of professional experience, I have contributed to various cutting-edge software products in network security, mobile apps, and healthcare software at renowned companies like Oracle, Yahoo, and Epic. My academic journey has taken me to prestigious institutions such as the University of Wisconsin-Madison and BITS Pilani in India, where I consistently ranked among the top of my class. At my core, I am a computer enthusiast with a profound interest in understanding the intricacies of computer programming. My skills are not limited to application programming in Java; I have also delved deeply into computer hardware, learning about various architectures, low-level assembly programming, Linux kernel implementation, and writing device drivers. The contributions of Linus Torvalds, Ken Thompson, and Dennis Ritchie—who revolutionized the computer industry—inspire me. I believe that real contributions to computer science are made by mastering all levels of abstraction and understanding systems inside out. In addition to my professional pursuits, I am passionate about teaching and sharing knowledge. I have spent two years as a teaching assistant at UW Madison, where I taught complex concepts in operating systems, computer graphics, and data structures to both graduate and undergraduate students. Currently, I am an assistant professor at KIIT, Bhubaneswar, where I continue to teach computer science to undergraduate and graduate students. I am also working on writing a few free books on systems programming, as I believe in freely sharing knowledge to empower others.