Exception Handling in Java — try catch finally, Custom Exceptions & Best Practices

👁️ 119 Views
|
📅 Jul 18, 2026
|
⏱️ 15 min read
Exception Handling in Java — try catch finally, Custom Exceptions & Best Practices
Head First Java, Third Edition
✦ Developer Pick

Head First Java, Third Edition

Ready to learn Java? This book combines puzzles, strong visuals, mysteries, and soul-searching interviews with famous Java objects to engage you in many different ways.

Every program you write will eventually encounter something unexpected — a file that does not exist, a network connection that times out, a user who enters letters where numbers were expected. How your code handles these situations is what separates a robust application from one that crashes and leaves users staring at a blank screen.

Exception handling is Java's built-in mechanism for dealing with these unexpected situations gracefully. It is also one of the most consistently tested topics in Java interviews at every level. In this guide we will cover everything — from the basic syntax to custom exceptions, checked vs unchecked, throw vs throws, and the best practices that separate clean Java code from messy code.

What Is an Exception in Java?

An exception is an event that occurs during the execution of a program that disrupts the normal flow of instructions. When an error occurs inside a method, Java creates an exception object containing information about the error — its type, the state of the program when the error occurred, and where it happened.

Without exception handling, your program would simply crash the moment something went wrong. With it, you can catch the error, handle it appropriately, log it, show the user a meaningful message, and continue running — or shut down cleanly if necessary.

// Without exception handling — program crashes
int[] numbers = {1, 2, 3};
System.out.println(numbers[5]); // ArrayIndexOutOfBoundsException — program stops here

// The rest of your code never runs
System.out.println("This line never executes");

The Exception Hierarchy in Java

In Java, everything that can be thrown is a subclass of Throwable. Understanding the hierarchy is important because it determines what you can catch and how:

Throwable
├── Error                          // Serious problems — do NOT catch these
│   ├── OutOfMemoryError
│   ├── StackOverflowError
│   └── VirtualMachineError
└── Exception                      // Recoverable problems — handle these
    ├── RuntimeException            // Unchecked exceptions
    │   ├── NullPointerException
    │   ├── ArrayIndexOutOfBoundsException
    │   ├── ClassCastException
    │   └── NumberFormatException
    └── IOException                 // Checked exceptions
        ├── FileNotFoundException
        └── SQLException

Errors represent serious problems in the JVM itself — like running out of memory or a stack overflow. These are generally not recoverable and you should not try to catch them.

Exceptions represent recoverable problems in your application logic. These are what you handle with try-catch blocks.

try, catch, and finally — The Basic Syntax

The fundamental building blocks of Java exception handling are three keywords: try, catch, and finally.

try {
    // Code that might throw an exception
    int result = 10 / 0; // ArithmeticException

} catch (ArithmeticException e) {
    // Code that runs if the exception occurs
    System.out.println("Cannot divide by zero: " + e.getMessage());

} finally {
    // Code that ALWAYS runs — whether exception occurred or not
    System.out.println("This always executes — use for cleanup");
}

Here is what each block does:

  • try — wraps the code that might throw an exception. As soon as an exception occurs, execution jumps immediately to the matching catch block. Any remaining code in the try block is skipped.
  • catch — catches a specific type of exception and handles it. You must specify which exception type you are catching. The variable e gives you access to the exception object and its details.
  • finally — runs no matter what happens. Whether the try block succeeded, threw an exception, or even if the catch block itself throws an exception — finally always runs. Use it for cleanup code like closing database connections or file streams.

Multiple catch Blocks

A single try block can have multiple catch blocks for different exception types. Java checks them in order from top to bottom and runs the first one that matches:

try {
    String str = null;
    int[] arr = new int[5];

    System.out.println(str.length());    // NullPointerException
    System.out.println(arr[10]);         // ArrayIndexOutOfBoundsException

} catch (NullPointerException e) {
    System.out.println("Null pointer: " + e.getMessage());

} catch (ArrayIndexOutOfBoundsException e) {
    System.out.println("Array index out of bounds: " + e.getMessage());

} catch (Exception e) {
    // Generic catch — catches anything not caught above
    System.out.println("Something went wrong: " + e.getMessage());

} finally {
    System.out.println("Cleanup done");
}

Important rule: Always put more specific exception types before more general ones. If you put Exception first, it catches everything and your specific catch blocks below it will never run. Java will actually give you a compile error if it detects this mistake.

Multi-catch (Java 7+)

If you want to handle multiple exception types the same way, you can catch them together using the pipe character:

try {
    // code that might throw either exception

} catch (NullPointerException | ArrayIndexOutOfBoundsException e) {
    System.out.println("Either null pointer or array issue: " + e.getMessage());
}

Checked vs Unchecked Exceptions

This is one of the most commonly asked topics in Java interviews. The difference is fundamental to how Java forces you to write safe code.

Checked Exceptions

Checked exceptions are exceptions that the Java compiler forces you to handle. If you call a method that can throw a checked exception, you MUST either wrap it in a try-catch or declare it in the method signature using throws. If you do neither, your code will not compile.

import java.io.*;

// ❌ This will NOT compile — FileNotFoundException is checked
public void readFile() {
    FileReader file = new FileReader("data.txt"); // compile error
}

// ✅ Option 1 — handle it with try-catch
public void readFile() {
    try {
        FileReader file = new FileReader("data.txt");
    } catch (FileNotFoundException e) {
        System.out.println("File not found: " + e.getMessage());
    }
}

// ✅ Option 2 — declare it with throws
public void readFile() throws FileNotFoundException {
    FileReader file = new FileReader("data.txt");
    // Now the caller is responsible for handling it
}

Common checked exceptions: IOException, FileNotFoundException, SQLException, ClassNotFoundException, ParseException.

Unchecked Exceptions

Unchecked exceptions (also called Runtime exceptions) are not enforced by the compiler. Your code will compile fine even if you do not handle them. They typically represent programming mistakes — bugs that should be fixed rather than caught.

// These compile fine without try-catch — but crash at runtime if they occur
String str = null;
str.length();          // NullPointerException at runtime

int[] arr = new int[3];
arr[10] = 5;           // ArrayIndexOutOfBoundsException at runtime

String s = "hello";
int n = Integer.parseInt(s); // NumberFormatException at runtime

Common unchecked exceptions: NullPointerException, ArrayIndexOutOfBoundsException, ClassCastException, NumberFormatException, IllegalArgumentException, ArithmeticException.

Checked Unchecked
Compiler enforced ✅ Yes ❌ No
Extends Exception RuntimeException
Must handle? ✅ Yes or declare ❌ Optional
Represents External issues (IO, DB) Programming bugs
Examples IOException, SQLException NullPointerException, ArrayIndexOutOfBoundsException

throw vs throws — The Difference

These two keywords confuse almost every Java beginner because they look so similar. Here is the clear distinction:

throw — manually throw an exception

Used inside a method body to manually throw an exception object:

public void setAge(int age) {
    if (age < 0 || age > 150) {
        throw new IllegalArgumentException("Age must be between 0 and 150. Got: " + age);
    }
    this.age = age;
}

// Calling the method
setAge(-5); // throws IllegalArgumentException with your custom message

throws — declare that a method might throw an exception

Used in the method signature to tell the caller that this method might throw a checked exception — and that the caller needs to handle it:

// throws declares what this method might throw
public void readFile(String path) throws IOException, FileNotFoundException {
    FileReader reader = new FileReader(path);
    // ...
}

// The caller MUST handle it
public void processFile() {
    try {
        readFile("data.txt");
    } catch (IOException e) {
        System.out.println("Could not read file: " + e.getMessage());
    }
}

Simple way to remember it: throw is an action (you throw something). throws is a declaration (you warn others about what might happen).

Creating Custom Exceptions

Java lets you create your own exception classes for domain-specific errors. This makes your code much more readable and meaningful. Creating a custom exception is straightforward — just extend Exception for checked, or RuntimeException for unchecked:

// Custom checked exception
public class InsufficientFundsException extends Exception {

    private double amount;

    public InsufficientFundsException(double amount) {
        super("Insufficient funds. Short by: " + amount);
        this.amount = amount;
    }

    public double getAmount() {
        return amount;
    }
}

// Using the custom exception
public class BankAccount {
    private double balance = 500.0;

    public void withdraw(double amount) throws InsufficientFundsException {
        if (amount > balance) {
            throw new InsufficientFundsException(amount - balance);
        }
        balance -= amount;
        System.out.println("Withdrawn: " + amount + " | Balance: " + balance);
    }
}

// Calling it
public class Main {
    public static void main(String[] args) {
        BankAccount account = new BankAccount();
        try {
            account.withdraw(1000.0);
        } catch (InsufficientFundsException e) {
            System.out.println(e.getMessage());
            System.out.println("You need: " + e.getAmount() + " more");
        }
    }
}

try-with-resources (Java 7+)

Before Java 7, you had to manually close resources like file streams, database connections, and network sockets in the finally block. This was repetitive and easy to forget. Java 7 introduced try-with-resources — a cleaner syntax that automatically closes resources when the try block finishes:

// Old way — manual close in finally (verbose and error-prone)
FileReader reader = null;
try {
    reader = new FileReader("data.txt");
    // read file...
} catch (IOException e) {
    e.printStackTrace();
} finally {
    if (reader != null) {
        try {
            reader.close(); // even this can throw an exception!
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

// New way — try-with-resources (Java 7+)
try (FileReader reader = new FileReader("data.txt")) {
    // read file...
    // reader.close() is called automatically when try block ends
} catch (IOException e) {
    System.out.println("Error reading file: " + e.getMessage());
}

Any class that implements the AutoCloseable interface can be used in try-with-resources. You can even open multiple resources in the same try statement:

try (
    FileReader reader = new FileReader("input.txt");
    FileWriter writer = new FileWriter("output.txt")
) {
    // Both reader and writer close automatically
} catch (IOException e) {
    e.printStackTrace();
}

Most Common Java Exceptions and What Causes Them

NullPointerException

// Cause — calling a method or accessing a field on a null reference
String str = null;
str.length(); // NullPointerException

// Fix — check for null before using
if (str != null) {
    str.length();
}

// Better fix (Java 8+) — use Optional
Optional.ofNullable(str).ifPresent(s -> System.out.println(s.length()));

ArrayIndexOutOfBoundsException

// Cause — accessing an index that does not exist
int[] arr = new int[3]; // valid indices: 0, 1, 2
arr[5] = 10;            // ArrayIndexOutOfBoundsException

// Fix — always check array length
if (index < arr.length) {
    arr[index] = 10;
}

NumberFormatException

// Cause — trying to convert a non-numeric string to a number
String input = "hello";
int number = Integer.parseInt(input); // NumberFormatException

// Fix — always handle this when parsing user input
try {
    int number = Integer.parseInt(input);
} catch (NumberFormatException e) {
    System.out.println("Please enter a valid number");
}

ClassCastException

// Cause — invalid type casting
Object obj = "Hello";
Integer num = (Integer) obj; // ClassCastException

// Fix — use instanceof before casting
if (obj instanceof Integer) {
    Integer num = (Integer) obj;
}

Exception Handling Best Practices

  • Never catch Exception or Throwable blindly — catching the broadest type hides real bugs. Always catch the most specific exception type possible.
  • Never swallow exceptions silently — an empty catch block is one of the worst things you can do. At minimum, log the exception so you know it happened.
  • Always log exceptions — use a proper logging framework like SLF4J or Log4j. Do not just print to console in production code.
  • Use finally or try-with-resources for cleanup — always close connections, streams, and resources properly to avoid memory leaks.
  • Create meaningful custom exceptions — a PaymentFailedException is far more useful than a generic RuntimeException with a message string.
  • Do not use exceptions for control flow — exceptions are for exceptional situations, not for regular logic branching. They are expensive to create because they capture the entire stack trace.
// ❌ Bad — catching everything silently
try {
    processPayment();
} catch (Exception e) {
    // says nothing, hides the problem
}

// ❌ Bad — using exceptions for normal flow
try {
    int value = Integer.parseInt(userInput);
} catch (NumberFormatException e) {
    return 0; // using exception as an if-else — avoid this
}

// ✅ Good — specific exception, logged properly
try {
    processPayment();
} catch (PaymentFailedException e) {
    logger.error("Payment failed for user {}: {}", userId, e.getMessage());
    throw new ServiceException("Payment could not be processed", e);
}

Quick Interview Questions on Exception Handling

These are the most commonly asked exception handling questions in Java interviews:

  • What is the difference between checked and unchecked exceptions? — Checked exceptions are enforced by the compiler; unchecked extend RuntimeException and are not.
  • Can finally block be skipped? — Yes, only if System.exit() is called or the JVM crashes. In all other cases finally always runs.
  • What is the difference between throw and throws?throw is used to actually throw an exception instance. throws is used in the method signature to declare what might be thrown.
  • Can we have a try block without a catch block? — Yes, if there is a finally block. try-finally is valid without catch.
  • What happens if an exception is thrown in finally? — It replaces the original exception, which is lost. This is why try-with-resources is preferred — it handles this automatically.
  • What is exception chaining? — Wrapping one exception inside another to preserve the original cause while throwing a higher level exception. Done using the constructor that accepts a Throwable cause parameter.

Final Thought

Exception handling is not just a feature to learn for interviews — it is a fundamental part of writing production-ready Java code. The difference between a Java developer who writes fragile code and one who writes robust code often comes down to how thoughtfully they handle exceptions.

Start with the basics — try, catch, finally. Then learn the checked vs unchecked distinction deeply because it comes up constantly. Once you are comfortable with those, start creating custom exceptions for your own projects. They make your code dramatically more readable and maintainable.

Have a specific exception scenario in your Java project that you are not sure how to handle? Drop us a message on our contact page — we will help you work through it.

Head First Java, Third Edition
✦ Developer Pick

Head First Java, Third Edition

Ready to learn Java? This book combines puzzles, strong visuals, mysteries, and soul-searching interviews with famous Java objects to engage you in many different ways.

Subscribe to Our Newsletter

Join Our Developer Community!

Unsubscribe Anytime | No Spam