Skip to content

Java Exceptions

An exception is an event that occurs during program execution that disrupts the normal flow of instructions. Java’s exception handling mechanism provides a robust way to manage these events.

Common causes of exceptions:

  • Invalid user input (e.g., entering text where a number is expected).
  • Resource issues (e.g., file not found, network connection lost, database unavailable).
  • Programming errors (e.g., accessing an array element out of bounds, trying to use a null reference).
  • Resource exhaustion (e.g., running out of memory).

Java classifies throwable objects (subclasses of java.lang.Throwable) into three main categories:

  • Checked Exceptions: Subclasses of Exception (excluding RuntimeException and its subclasses). These represent exceptional conditions that a well-written application should anticipate and recover from (e.g., IOException, FileNotFoundException, SQLException). The Java compiler requires that you handle checked exceptions, either by catching them using a try-catch block or by declaring them in the method signature using the throws keyword (the “Handle or Declare Rule”).
  • Unchecked Exceptions (Runtime Exceptions): Subclasses of RuntimeException. These typically indicate programming errors (e.g., NullPointerException, ArrayIndexOutOfBoundsException, IllegalArgumentException, ClassCastException). The compiler does not require you to handle or declare unchecked exceptions, although you can catch them if needed.
  • Errors: Subclasses of Error. These represent serious problems that are usually beyond the control of the application and from which recovery is generally not possible (e.g., OutOfMemoryError, StackOverflowError, NoClassDefFoundError). Applications typically do not try to catch Errors.

All exception and error types inherit from the java.lang.Throwable class.

`Throwable`
├── `Error` (Unrecoverable problems, usually not caught)
│ ├── `VirtualMachineError` (e.g., `OutOfMemoryError`, `StackOverflowError`)
│ └── ... (other critical errors)
└── `Exception` (Conditions that applications might want to catch)
├── `IOException` (Checked - Related to I/O failures)
│ └── `FileNotFoundException` (Checked)
├── `SQLException` (Checked - Related to database access)
├── `RuntimeException` (Unchecked - Programming errors or unexpected conditions)
│ ├── `NullPointerException`
│ ├── `ArrayIndexOutOfBoundsException`
│ ├── `IllegalArgumentException`
│ ├── `ClassCastException`
│ └── ... (other runtime exceptions)
└── ... (other checked exceptions like `ClassNotFoundException`)

Useful methods available on any exception or error object:

MethodDescription
String getMessage()Returns the detail message string describing the exception.
String toString()Returns a short description of the throwable (class name + result of getMessage()).
void printStackTrace()Prints the throwable’s stack trace (the sequence of method calls leading to the exception) to the standard error stream. Very useful for debugging.
void printStackTrace(PrintStream s) / void printStackTrace(PrintWriter s)Prints the stack trace to the specified stream/writer.
StackTraceElement[] getStackTrace()Returns an array of stack trace elements, allowing programmatic access to the stack information.
Throwable getCause()Returns the underlying cause of this throwable (if one was specified, used for exception chaining), or null.

Java provides keywords to handle exceptions:

  • try: Encloses the block of code (protected code) that might throw an exception.
  • catch: Follows a try block. Catches and handles a specific type of exception. A try block can have multiple catch blocks to handle different exception types.
  • finally: Follows a try block and any catch blocks. The code inside finally always executes, regardless of whether an exception was thrown or caught in the try block. It’s typically used for cleanup operations (like closing files or network connections), though try-with-resources is now often preferred for resource management.

Basic Syntax:

try {
// Code that might throw an exception(s)
// ...
} catch (SpecificExceptionType1 e1) {
// Handle SpecificExceptionType1
System.err.println("Caught exception 1: " + e1.getMessage());
// e1.printStackTrace(); // Often useful for debugging
} catch (SpecificExceptionType2 e2) {
// Handle SpecificExceptionType2
} catch (Exception e) {
// Catch broader exceptions (like Exception) last
// Handle any other Exception subclass not caught above
} finally {
// Cleanup code that always executes
// e.g., close resources (if not using try-with-resources)
System.out.println("Finally block executed.");
}
// Code here executes after try-catch-finally completes (unless program exits)

Example: Catching ArrayIndexOutOfBoundsException

Section titled “Example: Catching ArrayIndexOutOfBoundsException”

This code attempts to access an invalid array index, causing an exception.

// File: ExceptionDemo.java
public class ExceptionDemo {
public static void main(String[] args) {
try {
int[] numbers = new int[2]; // Array has indices 0 and 1
System.out.println("Accessing element at index 3...");
numbers[3] = 10; // This line will throw an exception
System.out.println("This line will not be executed.");
} catch (ArrayIndexOutOfBoundsException e) {
// Catch the specific exception
System.err.println("Error: Array index is out of bounds!");
System.err.println("Exception details: " + e); // Uses toString()
// e.printStackTrace(); // Uncomment for full stack trace
} finally {
System.out.println("The 'finally' block is always executed.");
}
System.out.println("Program continues after the try-catch-finally block.");
}
}

Output:

Accessing element at index 3...
Error: Array index is out of bounds!
Exception details: java.lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 2
The 'finally' block is always executed.
Program continues after the try-catch-finally block.

You can have multiple catch blocks to handle different types of exceptions. The first catch block whose parameter type matches (or is a superclass of) the thrown exception type is executed. Order matters: catch more specific exceptions before more general ones.

Java 7 introduced multi-catch, allowing you to catch multiple exception types in a single block if they share the same handling logic:

try {
// Code that might throw IOException or SQLException
} catch (IOException | SQLException ex) {
// Handle both IOException and SQLException here
logger.error("I/O or SQL error: ", ex);
// Note: 'ex' is implicitly final in a multi-catch block
}

Java 7 introduced the try-with-resources statement, which automatically closes resources (objects that implement java.lang.AutoCloseable or java.io.Closeable, like streams, sockets, database connections) when the try block finishes, whether normally or due to an exception. This is the preferred way to manage resources, as it makes code cleaner and less error-prone than using finally blocks for closing.

Syntax:

// Resource(s) declared in parentheses after try
try (FileInputStream fis = new FileInputStream("file.txt");
BufferedReader reader = new BufferedReader(new InputStreamReader(fis))) {
// Use the resources (fis, reader)
String line = reader.readLine();
// ... process file ...
} catch (IOException e) {
// Handle potential exceptions during resource usage or closing
System.err.println("Error processing file: " + e.getMessage());
}
// 'reader' and 'fis' are automatically closed here, even if exceptions occur.

If a method might throw a checked exception but does not handle it with try-catch, it must declare that it might throw the exception using the throws keyword in its signature. This informs callers that they need to handle or declare the exception themselves.

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;
public class FileReaderUtil {
// This method declares that it can throw IOException
public List<String> readFile(String filePath) throws IOException {
// Files.readAllLines() can throw IOException (a checked exception)
return Files.readAllLines(Paths.get(filePath));
}
public void processFile() {
try {
List<String> lines = readFile("data.txt"); // Caller must handle IOException
// ... process lines ...
} catch (IOException e) {
System.err.println("Failed to read file: " + e.getMessage());
}
}
}

A method can declare multiple exceptions separated by commas (e.g., throws IOException, SQLException). You generally don’t declare RuntimeExceptions or Errors using throws.

You can explicitly throw an exception object using the throw keyword. This is often done to signal an error condition discovered within your code.

public void setAge(int age) {
if (age < 0) {
// Throw an unchecked exception for invalid argument
throw new IllegalArgumentException("Age cannot be negative: " + age);
}
this.age = age;
}

You can throw newly created exception objects or re-throw an exception caught in a catch block.

You can create your own custom exception classes by extending Exception (for checked exceptions) or RuntimeException (for unchecked exceptions). This allows you to create specific exception types relevant to your application’s domain.

Guidelines for Custom Exceptions:

  • Extend Exception for conditions you expect callers to reasonably recover from (checked).
  • Extend RuntimeException for programming errors or unexpected conditions (unchecked).
  • Follow standard naming conventions (e.g., end class names with Exception).
  • Provide constructors that match the superclass constructors (e.g., a no-arg constructor, one taking a message String, one taking a message and a cause Throwable).
  • Include a serialVersionUID if the custom exception needs to be serializable.

Example: Custom Checked Exception

// File: InsufficientFundsException.java
public class InsufficientFundsException extends Exception { // Checked exception
private static final long serialVersionUID = 1L;
private final double shortfall;
public InsufficientFundsException(String message, double shortfall) {
super(message); // Pass message to Exception constructor
this.shortfall = shortfall;
}
public double getShortfall() {
return shortfall;
}
}
// File: BankAccount.java
public class BankAccount {
private double balance;
// ... constructor, deposit ...
public void withdraw(double amount) throws InsufficientFundsException {
if (amount < 0) {
throw new IllegalArgumentException("Withdrawal amount cannot be negative.");
}
if (balance >= amount) {
balance -= amount;
} else {
double needs = amount - balance;
throw new InsufficientFundsException("Insufficient funds for withdrawal", needs);
}
}
// ... getBalance ...
}
// Usage:
try {
myAccount.withdraw(1000.00);
} catch (InsufficientFundsException e) {
System.err.println(e.getMessage() + ". Short by: " + e.getShortfall());
} catch (IllegalArgumentException iae) {
System.err.println("Invalid withdrawal amount: " + iae.getMessage());
}
  • Use checked exceptions for recoverable conditions and unchecked exceptions for programming errors.
  • Don’t catch Throwable, Error, or overly broad exceptions like Exception unless absolutely necessary and you know how to handle them.
  • Use try-with-resources for managing resources.
  • Provide meaningful messages in exceptions.
  • Use exception chaining (new CustomException("message", cause)) to preserve the original exception context when wrapping exceptions.
  • Don’t ignore exceptions (empty catch blocks are usually bad practice). Log them or handle them appropriately.
  • Clean up resources in finally blocks only if try-with-resources cannot be used.
  • Consider using Optional<T> (Java 8+) as an alternative to returning null or throwing NullPointerException in some scenarios.