Exception Handling in Java

Afzal Badshah, PhD
4 min readDec 5, 2024

Exception handling is a crucial part of building robust and error-resilient Java applications. It allows developers to manage runtime errors effectively, ensuring the program can handle unexpected situations gracefully without crashing. You can visit the detailed tutorial on Comprehensive Guide to Object-Oriented Programming (OOP) in Java — Afzal Badshah, PhD.

This tutorial explores the principles, mechanisms, and strategies for managing runtime errors in Java, accompanied by examples for clarity.

Principles of Exception Handling

The main principles of exception handling are:

  1. Separation of Error Handling from Normal Logic: Code for handling exceptions is separated from the code for normal operations, enhancing readability and maintainability.
  2. Error Propagation: Exceptions can propagate up the call stack, allowing higher-level code to handle errors when appropriate.
  3. Graceful Degradation: Even in the presence of errors, the program continues to function, albeit with reduced capabilities.

Exception Hierarchy in Java

Java’s exception classes are organized in a hierarchy, rooted in the Throwable class:

  • Throwable
  • Error: Represents serious issues that are not usually recoverable (e.g., OutOfMemoryError).
  • Exception: Represents conditions that the application can recover from (e.g., IOException).

The Exception class is further divided:

  • Checked Exceptions: Must be declared or handled (e.g., IOException, SQLException).
  • Unchecked Exceptions (RuntimeExceptions): Need not be declared or handled (e.g., NullPointerException, ArithmeticException).

Mechanisms of Exception Handling in Java

Java provides several keywords to handle exceptions:

a. try-catch Block

A try block contains code that may throw exceptions, while catch blocks handle specific exceptions.

Example:

public class TryCatchExample {
public static void main(String[] args) {
try {
int result = 10 / 0; // This will throw ArithmeticException
} catch (ArithmeticException e) {
System.out.println("Cannot divide by zero. Error: " + e.getMessage());
}
}
}

b. finally Block

The finally block contains code that always executes, regardless of whether an exception occurred.

Example:

public class FinallyExample {
public static void main(String[] args) {
try {
int result = 10 / 2;
System.out.println("Result: " + result);
} catch (ArithmeticException e) {
System.out.println("Error occurred.");
} finally {
System.out.println("Cleanup operations, if any, are performed here.");
}
}
}

c. Throwing Exceptions (throw Keyword)

The throw keyword is used to explicitly throw an exception.

Example:

public class ThrowExample {
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) {
validateAge(16);
}
}

d. Declaring Exceptions (throws Keyword)

The throws keyword specifies that a method might throw an exception.

Example:

import java.io.IOException;
public class ThrowsExample {
public static void readFile() throws IOException {
throw new IOException("File not found.");
}
public static void main(String[] args) {
try {
readFile();
} catch (IOException e) {
System.out.println("Handled Exception: " + e.getMessage());
}
}
}

Exception Handling Strategies

a. Catch Specific Exceptions First

Always catch specific exceptions before more general exceptions to avoid masking specific errors.

Example:

try {
int result = 10 / 0;
} catch (ArithmeticException e) {
System.out.println("Arithmetic Exception caught.");
} catch (Exception e) {
System.out.println("General Exception caught.");
}

b. Avoid Swallowing Exceptions

Always log or handle exceptions to avoid silent failures.

Example:

try {
int result = 10 / 0;
} catch (ArithmeticException e) {
System.out.println("Error: " + e.getMessage()); // Good practice
}

c. Use Custom Exceptions

Create custom exceptions for application-specific errors.

Example:

class InvalidInputException extends Exception {
public InvalidInputException(String message) {
super(message);
}
}
public class CustomExceptionExample {
public static void validateInput(int value) throws InvalidInputException {
if (value < 0) {
throw new InvalidInputException("Value must be non-negative.");
}
}
public static void main(String[] args) {
try {
validateInput(-5);
} catch (InvalidInputException e) {
System.out.println("Caught Custom Exception: " + e.getMessage());
}
}
}

d. Avoid Overusing Exceptions

Use exceptions for exceptional conditions, not for regular program flow.

Example of Bad Practice:

try {
int[] arr = new int[5];
int value = arr[5]; // Throws ArrayIndexOutOfBoundsException
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("Index out of bounds.");
}

Better Practice:

if (index >= 0 && index < arr.length) {
int value = arr[index];
} else {
System.out.println("Index out of bounds.");
}

Advanced Exception Handling

a. Multi-Catch Block

Java allows catching multiple exceptions in a single catch block using the | operator.

Example:

try {
int result = 10 / 0;
} catch (ArithmeticException | NullPointerException e) {
System.out.println("Caught Exception: " + e.getMessage());
}

b. Rethrowing Exceptions

You can rethrow an exception after handling it partially.

Example:

public class RethrowExample {
public static void process() throws Exception {
try {
throw new Exception("Error occurred.");
} catch (Exception e) {
System.out.println("Logging the error: " + e.getMessage());
throw e; // Rethrowing the exception
}
}
    public static void main(String[] args) {
try {
process();
} catch (Exception e) {
System.out.println("Final Handling: " + e.getMessage());
}
}
}

c. Try-with-Resources

Used for handling resources like files, ensuring they are closed automatically.

Example:

import java.io.*;
public class TryWithResourcesExample {
public static void main(String[] args) {
try (BufferedReader reader = new BufferedReader(new FileReader("test.txt"))) {
System.out.println(reader.readLine());
} catch (IOException e) {
System.out.println("Error: " + e.getMessage());
}
}
}

Conclusion

Effective exception handling is a cornerstone of robust application development. By adhering to best practices and leveraging Java’s powerful exception-handling mechanisms, you can ensure that your applications are resilient and maintainable.

Key takeaways:

  • Use try-catch blocks to handle exceptions.
  • Always clean up resources using finally or try-with-resources.
  • Create custom exceptions for specific scenarios.
  • Avoid using exceptions for normal program flow.

Mastering exception handling will help you build applications that gracefully handle unexpected scenarios without compromising the user experience.

--

--

Afzal Badshah, PhD
Afzal Badshah, PhD

Written by Afzal Badshah, PhD

Dr Afzal Badshah focuses on academic skills, pedagogy (teaching skills) and life skills.

No responses yet