Throwing and Catching Exceptions Tutorial

Anastasios Antoniadis

An exception in Java is an unexpected event that occurs during the execution of a program, disrupting the normal flow of instructions. Exceptions typically arise due to runtime errors, such as dividing by zero, accessing an invalid array index, or attempting to read a non-existent file.

Why Do Exceptions exist?

Exceptions in Java exist to provide a structured, efficient way to handle runtime errors and exceptional conditions that can occur during program execution. Instead of relying on error codes or manual checks, Java uses exceptions to separate error-handling logic from regular code, improving readability and maintainability.

The exception-handling mechanism allows developers to catch and handle errors gracefully using try-catch blocks, propagate errors using throws, or define custom exceptions for specific use cases. This prevents abrupt program termination and enables recovery strategies, ensuring robustness and reliability in Java applications.

Java Exception Hierarchy

In Java, exceptions are part of the Throwable class hierarchy, which branches into two main categories: Error and Exception.

Error represents serious system-level issues (e.g., OutOfMemoryError, StackOverflowError) that applications typically cannot recover from.

Exception, on the other hand, represents conditions an application might handle. It further splits into checked exceptions (e.g., IOException, SQLException) and unchecked exceptions (i.e., runtime exceptions).

Checked exceptions must be explicitly handled using try-catch or declared with throws, while unchecked exceptions, under RuntimeException (e.g., NullPointerException, ArrayIndexOutOfBoundsException), usually indicate programming errors and do not require mandatory handling. This structured hierarchy helps Java enforce robust error management and exception handling.

Throwable
├── Error
│   └── ... (e.g., OutOfMemoryError)
└── Exception
    ├── IOException (Checked)
    ├── SQLException (Checked)
    ├── ClassNotFoundException (Checked)
    └── RuntimeException (Unchecked)
        ├── NullPointerException
        ├── ArithmeticException
        ├── ArrayIndexOutOfBoundsException
        └── ...

Checked vs. Unchecked Exceptions

Checked Exceptions

Checked exceptions in Java are exceptions that are checked at compile time, meaning the compiler ensures they are either handled using a try-catch block or declared using the throws keyword in the method signature.

These exceptions typically represent recoverable conditions that arise from external factors beyond the program’s control, such as file handling errors (IOException), database access issues (SQLException), or class-loading problems (ClassNotFoundException). Since they must be explicitly addressed in the code, checked exceptions encourage developers to implement proper error handling mechanisms to ensure application stability.

Unlike unchecked exceptions (RuntimeException and its subclasses), which usually result from programming errors, checked exceptions help in gracefully managing expected failures and maintaining robust application behavior.

For example, a method signature throwing a checked exception would look like:

public void readFile(String filePath) throws IOException {
    // code that might throw IOException
}

Unchecked Exceptions (Runtime Exceptions)

Unchecked exceptions in Java are exceptions that are not checked at compile time, meaning the compiler does not force the developer to handle them explicitly. These exceptions are subclasses of RuntimeException, which itself extends Exception. Common examples include NullPointerException, ArrayIndexOutOfBoundsException, and IllegalArgumentException.

Unchecked exceptions usually indicate programming logic errors, such as accessing an invalid array index or calling a method on a null reference. Since they arise due to coding mistakes rather than external factors, they do not require mandatory handling with try-catch blocks. Instead, developers are encouraged to write robust code that prevents these errors through proper validation and defensive programming techniques.

Since unchecked exceptions often indicate bugs, best practice is to fix the code that causes these rather than catch them frequently.

Using try-catch Blocks

When you suspect a statement could throw an exception, you wrap it in a try block and follow it with one or more catch blocks:

try {
    // Code that may throw an exception
    int result = 10 / 0;  // This will throw ArithmeticException
    System.out.println("Result: " + result);
} catch (ArithmeticException ex) {
    System.out.println("Caught an arithmetic exception: " + ex.getMessage());
}

Flow Explanation:

  1. try block: Executes the statements that may throw exceptions.
  2. catch block: If an exception specified in the catch parameter type occurs, control jumps here. You can have multiple catch blocks for different exception types.
  3. (Optional) finally block: Always executes, whether an exception occurs or not.

finally Block

The finally block is used for cleanup code that must be executed regardless of whether an exception is thrown. Typical examples include closing files, releasing resources, or cleaning up connections.

FileInputStream fis = null;
try {
    fis = new FileInputStream("data.txt");
    // read from file
} catch (FileNotFoundException e) {
    System.out.println("File not found: " + e.getMessage());
} finally {
    if (fis != null) {
        try {
            fis.close();
        } catch (IOException e) {
            // Log or handle the secondary exception
        }
    }
}

In this snippet:

  • If an exception is thrown in the try block, the catch block handles it.
  • The finally block always executes, ensuring the file is closed, whether reading from it was possible or not.

try-with-resources

A more modern approach to manage resources (files, streams, sockets, etc.) was introduced in Java 7 called try-with-resources. This automatically closes the resource after the try block ends, even if an exception is thrown:

try (FileInputStream fis = new FileInputStream("data.txt");
     FileOutputStream fos = new FileOutputStream("output.txt")) {
    // Read from fis, write to fos
} catch (IOException e) {
    e.printStackTrace();
}
  • All resources opened in the parentheses of the try statement must implement AutoCloseable (which most IO classes do).
  • No need for an explicit finally block to close resources.

Multiple Catch Blocks

When you have code that can throw different exceptions, you can use multiple catch blocks:

try {
    String text = null;
    System.out.println(text.length());  // Might throw NullPointerException

    int num = Integer.parseInt("ABC");  // Might throw NumberFormatException
} catch (NullPointerException ex) {
    System.out.println("Null reference encountered!");
} catch (NumberFormatException ex) {
    System.out.println("Invalid number format!");
}
  • Order matters when catching exceptions.
  • If you have a superclass exception type (Exception) and a subclass type (NumberFormatException), you must catch the subclass first. Otherwise, the code may not compile (unreachable catch block).

Catching Multiple Exceptions in One Block

Since Java 7, you can catch multiple exceptions in one catch block using the | operator:

try {
    // Code that might throw NullPointerException or NumberFormatException
} catch (NullPointerException | NumberFormatException ex) {
    System.out.println("Caught either NullPointerException or NumberFormatException: " 
                       + ex.getMessage());
}

This approach reduces code duplication when the handling logic is the same for multiple exception types.

Throwing Exceptions

You can explicitly throw an exception using the throw keyword. This is especially useful for validation or custom error conditions:

public static int divide(int numerator, int denominator) {
    if (denominator == 0) {
        throw new ArithmeticException("Cannot divide by zero");
    }
    return numerator / denominator;
}
  • throw is used to initiate an exception.
  • If this exception is checked, the method must declare it with throws in the method signature.

throws Clause

You need a throws clause in the method signature if your method can throw a checked exception and does not handle it. For example:

public static void readFile(String fileName) throws IOException {
    FileInputStream fis = new FileInputStream(fileName);
    // ...
}

Creating Custom Exceptions

You can create your own exception classes by extending:

  • Exception (for checked exceptions)
  • RuntimeException (for unchecked exceptions)

Example of a Custom Checked Exception

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

Example of a Custom Unchecked Exception

public class InvalidUserInputException extends RuntimeException {
    public InvalidUserInputException(String message) {
        super(message);
    }
}

Why create custom exceptions?

Creating custom exceptions in Java is useful when we need to define specific error conditions that are not adequately represented by built-in exceptions. Custom exceptions improve code readability and maintainability by clearly conveying the nature of an error within a particular application domain. For example, in a banking application, defining a InsufficientBalanceException makes it easier to understand and handle cases where a withdrawal amount exceeds the account balance.

Custom exceptions also allow us to encapsulate additional details about the error, such as error codes or context-specific messages, making debugging and logging more informative. By extending Exception (for checked exceptions) or RuntimeException (for unchecked exceptions), developers can create meaningful exception hierarchies that align with business logic, ensuring more structured and maintainable error handling.

Exceptions Best Practices

  1. Handle exceptions at the right level
    • Handle an exception where you can actually fix or manage the issue. If not, let it propagate by throwing it up the call stack.
  2. Use specific exceptions
    • Catching Exception or Throwable can make debugging harder and may mask other problems. Use the most specific exception types possible.
  3. Avoid empty catch blocks
    • If you catch an exception, always either handle it, log it, or re-throw it. Silently ignoring exceptions is dangerous.
  4. Don’t catch Error
    • Errors typically mean the JVM is in a state from which recovery is not possible. Let the JVM handle them.
  5. Clean up resources
    • Use try-with-resources or a finally block to ensure that resources (files, sockets, DB connections) are properly closed.
  6. Throw exceptions with context
    • Always include meaningful messages with your exceptions to aid debugging.
  7. Use Custom Exceptions Wisely
    • Create custom exceptions only when they add value and clarity over standard exceptions.

Putting It All Together – Example

public class ExceptionDemo {

    public static void main(String[] args) {
        try {
            processFile("nonexistent.txt");
        } catch (CustomFileNotFoundException e) {
            System.err.println("Custom File Error: " + e.getMessage());
        } catch (IOException e) {
            System.err.println("IO Error: " + e.getMessage());
        } finally {
            System.out.println("Process complete.");
        }
    }

    // Method that may throw a custom and an IO exception
    public static void processFile(String filePath) throws CustomFileNotFoundException, IOException {
        if (filePath.equals("nonexistent.txt")) {
            throw new CustomFileNotFoundException("File does not exist: " + filePath);
        }
        // Some code that might throw an IOException
        // For example, new FileInputStream(filePath)
        System.out.println("File processed successfully");
    }
}

// Custom checked exception
class CustomFileNotFoundException extends Exception {
    public CustomFileNotFoundException(String message) {
        super(message);
    }
}

This Java program demonstrates exception handling using both a custom exception (CustomFileNotFoundException) and a built-in exception (IOException).

In the main method, it attempts to process a file named "nonexistent.txt" by calling processFile. The processFile method checks if the given file path matches "nonexistent.txt", and if so, it throws a CustomFileNotFoundException, a custom checked exception that extends Exception. If this exception is thrown, it is caught in the main method, and an error message is printed.

Additionally, the method signature includes throws IOException, indicating that it might also throw an IOException, though it’s not explicitly thrown in this snippet (it could occur if real file operations were included).

The finally block ensures that "Process complete." is printed regardless of whether an exception occurs. This structured approach demonstrates the importance of custom exceptions for meaningful error messages and robust exception handling in Java programs.

Conclusion

Exception handling in Java is a powerful mechanism that separates error handling from normal code logic. By correctly identifying checked vs. unchecked exceptions, using trycatchfinally (or try-with-resources) blocks, and throwing custom exceptions where necessary, you can create robust, maintainable, and clear software.

Key takeaway: Always handle exceptions with the appropriate level of specificity, never leave them silently ignored, and ensure any resources are appropriately cleaned up. This approach leads to more reliable code and makes it easier for you or other developers to debug issues when they arise.

Anastasios Antoniadis
Find me on
Latest posts by Anastasios Antoniadis (see all)

Leave a Comment