Exception Handling in Java

Programming is a complex skill, and even experienced programmers can face challenges during development. Various issues can arise, including typing errors, compilation errors, linking errors, and runtime errors. The first three types of errors are generally easier to fix. However, when runtime errors occur during program execution, it’s essential to handle these situations gracefully. This chapter discusses how to manage such errors.

What are Exceptions?

Errors that happen while a program is running are called exceptions. There are many reasons why exceptions can occur, such as:

  1. Running out of memory
  2. Unable to open a file
  3. Exceeding the bounds of an array
  4. Trying to initialize an object with an invalid value
  5. Division by zero
  6. Stack overflow
  7. Arithmetic overflow or underflow
  8. Using an unassigned reference
  9. Unable to connect to a server

When exceptions occur, programmers must decide how to handle them. Strategies include:

  • Displaying error messages on the screen
  • Showing a dialog box in graphical user interfaces (GUIs)
  • Asking the user for better input
  • Terminating the program


Exception Handling in Java

Java provides a structured, object-oriented way to handle runtime errors. The exception handling mechanism in Java involves three keywords: throw, catch, and try.

When an error occurs during the execution of a function, Java informs the application that an error has happened. This process is known as throwing an exception. It consists of two steps:

  1. Creating an exception object that contains information about the error.
  2. Throwing this exception object using the throw keyword.

Java has built-in exception classes for different situations, such as divide-by-zero errors or file access issues. All these classes derive from a base class called Exception, which is further derived from the Object class. Programmers can also define their own exception classes for specific situations.

Using try and catch

The code that might cause an exception is placed in a try block. If an exception occurs, control passes to a section of code called the catch block, which handles the exception. Here’s a code snippet illustrating this:

package exceptionexampleproject;

public class ExceptionExampleProject {
    public static void main(String[] args) {
        // Normal code
        try {
            fun(); // Code that might cause an exception
        } catch (Exception e) { // Catch block to handle the exception
            // Handle the error
        }
    }

    public static void fun() {
        // If an error occurs, Java creates an exception object
        // and throws it, which is then caught in the catch block
    }
}

In this example, fun() is a method where an exception might occur, so it’s called within a try block. If an exception does happen, it’s caught in the catch block.

When handling a thrown exception, the program can do one of the following:

  1. Do nothing and let the default exception handler manage the error.
  2. Rectify the issue that caused the exception and continue execution.
  3. Exit gracefully.

Example Cases of Exception Handling

Case 1: Do nothing when an exception occurs

package case1project;
import java.io.*;

public class Case1Project {
    public static void main(String[] args) throws NumberFormatException {
        int num;
        try {
            BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
            System.out.print("Enter a number: ");
            num = Integer.parseInt(br.readLine());
            System.out.println(num);
        } catch (IOException e) {
            System.out.println("Error in input");
        }
    }
}

In this program, if the input is invalid (like “12a”), a NumberFormatException will occur. Since we haven’t caught this exception, the program will terminate and print a stack trace.

Case 2: Rectify and continue when an exception occurs

package case2project;
import java.io.*;

public class Case2Project {
    public static void main(String[] args) {
        int num;
        while (true) {
            try {
                BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
                System.out.println("Enter a number: ");
                num = Integer.parseInt(br.readLine());
                break; // Exit the loop if input is valid
            } catch (IOException e) {
                System.out.println("Error in input");
            } catch (NumberFormatException e) {
                System.out.println("Incorrect Input");
            }
        }
        System.out.println("You entered: " + num);
    }
}

In this program, if the user enters invalid input, the program prompts them to try again until a valid number is entered.

Case 3: Exit gracefully when an exception occurs

package case3project;
import java.io.*;

public class Case3Project {
    public static void main(String[] args) {
        int num;
        try {
            BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
            System.out.println("Enter a number: ");
            num = Integer.parseInt(br.readLine());
            System.out.println("You entered: " + num);
        } catch (IOException e) {
            System.out.println("Error in input");
        } catch (NumberFormatException e) {
            System.out.println("Incorrect Input");
        }
    }
}

In this case, if the user enters invalid input, the program prints a message and terminates without displaying a stack trace.


Handling Multiple Exceptions

You can catch multiple exceptions by providing separate catch blocks for each possible exception. Here’s an example:

package multipleexceptionsproject;
import java.io.*;

public class MultipleExceptionsProject {
    public static void main(String[] args) {
        int i, j;
        try {
            BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
            System.out.println("Enter i: ");
            i = Integer.parseInt(br.readLine());
            System.out.println("Enter j: ");
            j = Integer.parseInt(br.readLine());
            System.out.println("You entered: " + i + " " + j);
            System.out.println("Result: " + i / j);
        } catch (IOException e) {
            System.out.println("Error in input");
        } catch (NumberFormatException ne) {
            System.out.println("Incorrect Input");
        } catch (ArithmeticException ae) {
            System.out.println("Arithmetic Exception: Division by zero");
        } catch (Exception e) {
            System.out.println("Unknown Error: " + e);
        }
    }
}

In this program, there are separate catch blocks for each possible exception. Only one block will execute when an exception occurs, and the order of catch blocks is important—more specific exceptions should be caught before more general ones.


Using the finally Block

Sometimes, you want certain code to execute no matter what, even if an exception occurs. The finally block serves this purpose:

package finallyblockproject;
import java.io.*;

public class FinallyBlockProject {
    public static void main(String[] args) {
        FileWriter fw = null;
        try {
            fw = new FileWriter("a.txt");
            fw.write("Hello World\n");
        } catch (IOException ie) {
            System.out.println("Encountered IO Error");
        } finally {
            try {
                if (fw != null) {
                    fw.close();
                }
            } catch (IOException e) {
                System.out.println("Error while closing the file");
            }
        }
    }
}

In this example, the finally block ensures that the file is closed regardless of whether an exception occurred during writing. This is crucial for resource management.


User-defined Exceptions

Sometimes, the built-in exception classes don’t cover specific situations. In such cases, you can create your own exception classes. For example, in a banking application, you might need an exception when a withdrawal would drop the balance below a minimum limit. Here’s how you can define and use a user-defined exception:

package userdefinedexceptionproject;

class Customer {
    private String name;
    private int accno;
    private int balance;

    public Customer(String n, int a, int b) {
        name = n; 
        accno = a; 
        balance = b;
    }

    public void withdraw(int amt) throws BankException {
        if (balance - amt < 500) { // Check if balance will go below 500
            throw new BankException(accno, balance); // Throw custom exception
        }
        balance -= amt; // Update balance
    }
}

class BankException extends Exception {
    private int acc;
    private int bal;

    public BankException(int a, int b) {
        this.acc = a;
        this.bal = b;
    }

    public void inform() {
        System.out.println("Account No.: " + acc);
        System.out.println("Balance: " + bal);
    }
}

public class UserDefinedExceptionProject {
    public static void main(String[] args) {
        try {
            Customer c = new Customer("Rahul", 2453, 900);
            c.withdraw(450); // Attempt to withdraw
        } catch (BankException ex) {
            System.out.println("Transaction failed");
            ex.inform(); // Inform user of the failure
        }
    }
}

In this program, the Customer class has a withdraw method that checks if a withdrawal would cause the balance to drop below 500. If it would, it throws a BankException, which is handled in the

main method.


A More Practical Example

So far, we’ve looked at basic examples of exception handling. Now, let’s explore how to use exceptions in a practical scenario by implementing a stack data structure. We’ll handle errors in two situations:

  1. When we try to add more objects to the stack than it can hold.
  2. When we try to remove an object from an empty stack.

Here’s the code that uses exceptions to manage these errors:

// Use of exceptions to report errors while maintaining a stack
package stackswithexceptionsproject;

class Stack {
private int capacity;
private int size;
private Object[] data;

public Stack(int cap) {
data = new Object[cap];
capacity = cap;
size = 0;
}

public void push(Object o) throws StackException {
if (size == capacity) {
throw new StackException("Stack full");
}
data[size] = o;
size++;
}

public Object pop() throws StackException {
if (size <= 0) {
throw new StackException("Stack empty");
}
size--;
return data[size];
}

public int getSize() {
return size;
}
}

class StackException extends Exception {
private String errorMsg;

public StackException(String msg) {
this.errorMsg = msg;
}

public void inform() {
System.out.println(errorMsg);
}
}

public class StacksWithExceptionsProject {
public static void main(String[] args) {
Stack s = new Stack(3); // Create a stack with a capacity of 3
try {
s.push("Vinod");
s.push("Sanjay");
s.push(25);
s.push(3.14f); // This will throw an exception
} catch (StackException ex) {
System.out.println("Problem in stack");
ex.inform();
}

try {
while (s.getSize() > 0) {
System.out.println(s.pop());
}
} catch (StackException ex) {
System.out.println("Problem in stack");
ex.inform();
}
}
}

Output:

Problem in stack
Stack full
25
Sanjay
Vinod

In this example, an exception is triggered when trying to add too many objects to the stack. You can also experiment with creating a queue data structure similarly.

Generalizing User-defined Exceptions

From the banking and stack examples, we can summarize how to manage user-defined exceptions into four main parts:

  1. Define the Exception Class: We created classes like BankException and StackException that inherit from the Exception class. These classes have constructors to create exception objects and a method called inform() to display error messages.
  2. Throw an Exception: When an exceptional situation arises, we create and throw an exception object. For instance, in the banking example, a BankException is thrown if the balance goes below 500, while in the stack example, a StackException is thrown if we try to add to a full stack or remove from an empty one.
  3. The try Block: Code that may cause exceptions is enclosed in a try block. Only the code that might result in an exceptional condition needs to be included here.
  4. The Exception Handler (catch Block): Code that handles the exception is placed in a catch block, where we specify the exception type we want to catch.

How It Works

Here’s how the exception handling flow goes:

  1. Code executes normally outside the try block.
  2. Control enters the try block.
  3. A statement in the try block causes an error in a called member function.
  4. The member function creates and throws an exception object.
  5. Control transfers to the catch block that follows the try.

This structure keeps the code clean and organized, handling exceptions automatically.

Important Points to Note

  1. Avoid using a generic catch (Exception e) block to suppress errors without handling them properly.
  2. Strive to resolve exceptional situations or exit gracefully.
  3. Use multiple catch blocks to distinguish between different types of exceptions.
  4. The statement causing an exception may not necessarily be in the try block; it could be in a function called from the try.
  5. try blocks can be nested within each other.
  6. If an inner try block throws an exception, outer catch blocks are checked for a match.
  7. When creating a library for others, anticipate potential problems and throw exceptions accordingly.
  8. When using a library, be sure to include try and catch blocks for any exceptions it may throw.
  9. While exceptions are useful, they add overhead in program size and execution time, so use them judiciously.