Skip to main content

Java, Programming

Exception Handling in Java: Full Guide

·

Java exception handling concept showing a try block routing an error into a catch block

Java exception handling concept showing a try block routing an error into a catch block

Exception handling in Java is the mechanism that catches runtime errors and lets your program respond to them instead of crashing. A file goes missing, a network call times out, a user types a letter where a number belongs: each of these throws an exception, and the language gives you a structured way to catch it, react, and keep running. This guide covers the exception hierarchy, checked versus unchecked exceptions, try-catch-finally, throw and throws, custom exceptions, try-with-resources, and the mistakes that cause silent failures. If a Java assignment is failing on an error you cannot trace, our Java assignment help team works through the same patterns shown below.

What is an exception in Java?

An exception is an object that represents an event that disrupts the normal flow of a program. When the JVM hits a problem it cannot handle on its own, it creates an exception object, stops the current path of execution, and hands that object to the runtime to find a handler. The range runs from a divide-by-zero in one line of arithmetic to a failed database connection three method calls deep.

Every exception carries a message, a type, and a stack trace. The type tells you what category of failure occurred. The message adds detail. The stack trace records the exact chain of method calls that led to the failure, which is the single most useful debugging artifact Java gives you.

public class FirstException {
    public static void main(String[] args) {
        int[] numbers = {1, 2, 3};
        System.out.println(numbers[5]); // index 5 does not exist
    }
}

Running this prints an ArrayIndexOutOfBoundsException, names the bad index, and lists the line that threw it. The program then terminates because nothing caught the exception.

Checked vs unchecked exceptions

Java splits exceptions into two groups that the compiler treats differently: checked and unchecked. The split decides whether the compiler forces you to deal with the exception before your code will build.

Checked exceptions extend Exception but not RuntimeException. The compiler requires you to either catch them or declare them with throws. They model recoverable conditions outside your control, such as a missing file or a dropped connection. IOException, SQLException, and FileNotFoundException are checked. Skip the handling and the code does not compile.

Unchecked exceptions extend RuntimeException. The compiler asks nothing of you. They almost always signal a bug in the code itself: a null dereference, a bad cast, an index past the end of an array. You can catch them, but catching a NullPointerException usually hides a defect that you should fix instead.

| Property | Checked | Unchecked | | --- | --- | --- | | Base class | Exception | RuntimeException | | Compiler enforces handling | Yes | No | | Typical cause | External condition | Programming bug | | Examples | IOException, SQLException | NullPointerException, ArithmeticException | | Right response | Catch and recover | Fix the code |

The rule of thumb: throw a checked exception when the caller can reasonably recover, and an unchecked exception when the failure means the program was used incorrectly.

Common exceptions you will meet

Eight exceptions account for most of what beginners and intermediate developers run into, and recognizing them on sight saves debugging time.

  1. NullPointerException: you call a method or read a field on a reference that is null. The most frequent runtime failure in Java.
  2. ArithmeticException: an illegal arithmetic operation, most commonly integer division by zero.
  3. ArrayIndexOutOfBoundsException: you access an array element with an index below 0 or at or beyond the array length.
  4. ClassCastException: you cast an object to a type it is not an instance of.
  5. NumberFormatException: Integer.parseInt("abc") and similar parse calls fail on text that is not a number.
  6. FileNotFoundException: a checked exception thrown when a file you try to open does not exist or cannot be read.
  7. IOException: the broad checked parent for input and output failures across files, streams, and sockets.
  8. IllegalArgumentException: a method received an argument that breaks its contract, such as a negative count.

Six of these are unchecked and signal bugs in your own logic. Two, FileNotFoundException and IOException, are checked and signal a condition the environment handed you.

The exception hierarchy

All exceptions in Java descend from a single root, java.lang.Throwable, and the tree below it decides how the compiler and the JVM treat each type. Understanding the hierarchy turns exception handling from memorization into a system you can reason about.

java.lang.Throwable
├── java.lang.Error              (do not catch: OutOfMemoryError, StackOverflowError)
└── java.lang.Exception          (checked)
    └── java.lang.RuntimeException (unchecked)
        ├── NullPointerException
        ├── ArithmeticException
        └── ArrayIndexOutOfBoundsException

Throwable has two direct children. Error represents conditions a normal program cannot recover from, such as OutOfMemoryError and StackOverflowError; you do not catch these. Exception is the branch your code works with.

Checked branch: java.lang.Exception

The Exception class and its subclasses (excluding the RuntimeException subtree) are the checked exceptions. The compiler tracks them across method boundaries. Any code that can throw one must catch it with a try-catch block or pass the responsibility upward with a throws clause.

Unchecked branch: java.lang.RuntimeException

RuntimeException and everything below it are unchecked. These typically come from programming errors: dereferencing null, indexing out of bounds, dividing by zero. The compiler does not require handling because the correct response is usually to fix the code, not to wrap a bug in a catch block.

Java exception class hierarchy from Throwable down through Error, Exception, and RuntimeException

The try-catch-finally block

The try-catch block is the core tool of exception handling: it wraps risky code, redirects control to a handler when something throws, and keeps the program alive. An optional finally block runs cleanup either way.

try {
    // Code that might throw an exception
} catch (ExceptionType1 e1) {
    // Handle ExceptionType1
} catch (ExceptionType2 e2) {
    // Handle ExceptionType2
} finally {
    // Runs whether or not an exception occurred
}

The try block holds the code that might fail. If a statement throws, the JVM stops the block immediately and looks down the list of catch blocks for the first one whose type matches. Each catch block handles one category of exception. The finally block, when present, runs no matter what happens above it, which makes it the place for cleanup.

Handling an exception in a catch block

Inside a catch block you decide how the program responds: log the failure, show a clear message, or fall back to a default. Read the message off the exception object rather than guessing at the cause.

try {
    int result = 10 / divisor;
    System.out.println(result);
} catch (ArithmeticException e) {
    System.err.println("Cannot divide by zero: " + e.getMessage());
}

Order your catch blocks from most specific to most general. A handler for a parent type placed above a handler for its child shadows the child, and the compiler rejects the unreachable block.

Catching multiple exceptions in one block

Since Java 7, a single catch block handles several unrelated exception types separated by the pipe symbol. The multi-catch removes the duplicated handler code you would otherwise write twice.

try {
    // Code that might throw either exception
} catch (IOException | SQLException e) {
    System.err.println("Operation failed: " + e.getMessage());
}

The caught variable is effectively final in a multi-catch, so you cannot reassign e. The types you list must not be parent and child of each other, since the parent already covers the child.

Nesting try-catch blocks

A try-catch block placed inside another lets you handle a failure at a fine grain and still keep an outer safety net. The inner block deals with what it can; anything it does not match propagates to the outer block.

try {
    Connection conn = openConnection();
    try {
        runQuery(conn);
    } catch (SQLException e) {
        System.err.println("Query failed: " + e.getMessage());
    }
} catch (IOException e) {
    System.err.println("Connection failed: " + e.getMessage());
}

Deep nesting hurts readability fast. When you find yourself three levels in, extract the inner block into its own method and let the exceptions surface there instead.

The finally block and resource cleanup

The finally block runs after the try and any matching catch, whether or not an exception was thrown, which makes it the classic home for cleanup. It runs even when the try or catch block hits a return. Two cases skip it: a call to System.exit(), and the JVM or thread dying outright.

The verbose manual pattern

Before try-with-resources, closing a stream meant a nested null check and an inner try-catch, because close() can itself throw. This pattern is correct but noisy.

FileInputStream file = null;
try {
    file = new FileInputStream("example.txt");
    // Read from the file
} catch (IOException e) {
    System.err.println("Read failed: " + e.getMessage());
} finally {
    if (file != null) {
        try {
            file.close();
        } catch (IOException e) {
            System.err.println("Close failed: " + e.getMessage());
        }
    }
}

Ten lines of cleanup for one line of real work is the reason try-with-resources exists. The next section replaces all of it with three lines.

try-with-resources: the modern way to close

Try-with-resources, added in Java 7, declares one or more resources in parentheses after try and closes each one automatically when the block ends. Any object whose class implements AutoCloseable qualifies, which covers streams, readers, writers, JDBC Connection, and sockets.

try (BufferedReader reader = new BufferedReader(new FileReader("file.txt"))) {
    String line;
    while ((line = reader.readLine()) != null) {
        System.out.println(line);
    }
} catch (IOException e) {
    System.err.println("Read failed: " + e.getMessage());
}

The JVM closes the reader whether the block finishes normally or throws, and it closes multiple resources in the reverse of the order you declared them. If both the body and a close() call throw, the body's exception wins and the close() exception is attached as a suppressed exception, which you read back with getSuppressed(). This is the recommended pattern for every closeable resource. The same care over closing resources applies in file code more broadly, covered in our guide to Java file I/O.

throw and throws: raising and declaring exceptions

throw raises an exception, and throws declares one. They look alike and do opposite jobs: throw is an action inside a method body, while throws is a warning in a method signature.

Throwing an exception with throw

The throw statement raises a single exception object at the point you decide a condition is invalid. Validate an argument, throw when it fails, and the caller's catch block takes over.

public void setAge(int age) {
    if (age < 0) {
        throw new IllegalArgumentException("Age cannot be negative: " + age);
    }
    this.age = age;
}

Throw the most specific type that fits. IllegalArgumentException for a bad argument, IllegalStateException for an object used at the wrong time, and a custom type when the standard library has no good match.

Declaring exceptions with throws

The throws clause in a method signature lists the checked exceptions the method can let escape. It is a contract: every caller must either catch those types or declare them in turn.

public void readData(String filename) throws IOException {
    BufferedReader reader = new BufferedReader(new FileReader(filename));
    // Read data; an IOException here propagates to the caller
}

Anyone calling readData now sees that it can throw IOException and the compiler holds them to handling it. Declaring throws on unchecked exceptions is legal but adds nothing, since the compiler ignores it for that branch.

Custom exceptions

A custom exception is a class you write that extends Exception or RuntimeException to model a failure specific to your domain. Standard exceptions cover generic problems; a InsufficientFundsException says something RuntimeException never could.

public class InsufficientFundsException extends Exception {
    private final double shortfall;

    public InsufficientFundsException(String message, double shortfall) {
        super(message);
        this.shortfall = shortfall;
    }

    public double getShortfall() {
        return shortfall;
    }
}

Extend Exception to make it checked, so callers are forced to handle it, or extend RuntimeException to make it unchecked. Add a field like shortfall when the caller needs structured data about the failure rather than only a message string. Then throw it where the condition arises:

public void withdraw(double amount) throws InsufficientFundsException {
    if (amount > balance) {
        throw new InsufficientFundsException(
            "Withdrawal of " + amount + " exceeds balance " + balance,
            amount - balance);
    }
    balance -= amount;
}

Best practices for custom exceptions

Five habits keep custom exceptions useful instead of decorative.

  • Name for the failure. End the class in Exception and describe the condition: InvalidOrderException, not MyException.
  • Carry a clear message. Include the values that triggered the failure so the stack trace explains itself.
  • Preserve the cause. Add a constructor that takes a Throwable and passes it to super(message, cause) so wrapping does not erase the original trace.
  • Group related types. When several custom exceptions belong together, give them a shared parent so a caller can catch the family with one block.
  • Do not overdo it. Reserve custom exceptions for cases the standard library does not model. Reach for IllegalArgumentException before inventing a new type.

GeeksProgramming Java tutors reviewing custom exception class design on screen

Exception propagation and the call stack

Exception propagation is the process by which an uncaught exception moves up the call stack, method by method, until something catches it or the program ends. Each method call adds a frame to the stack; an exception unwinds those frames in search of a matching handler.

When code throws, the JVM checks the current method for a matching catch block. If there is none, it removes that method's frame and checks the caller, then the caller's caller, all the way to the entry point. The first matching handler stops the unwinding. If none exists, the exception becomes uncaught and the thread terminates.

public class PropagationExample {
    public static void main(String[] args) {
        try {
            methodA();
        } catch (IllegalStateException e) {
            System.out.println("Caught in main: " + e.getMessage());
        }
    }

    static void methodA() {
        methodB();
    }

    static void methodB() {
        throw new IllegalStateException("Failure originated in methodB");
    }
}

The exception starts in methodB, passes through methodA (which has no handler), and lands in main, which catches it. The getMessage() text and the full stack trace both point back to methodB, which is how propagation turns a deep failure into a readable report.

Exception handling best practices

Good exception handling makes failures visible and specific instead of hidden and vague. The same rules apply across web apps, command-line tools, and concurrent systems.

  • Catch the most specific type. Handle FileNotFoundException before IOException, and never reach for catch (Exception e) when a narrower type fits.
  • Never swallow an exception. An empty catch block hides the failure and leaves you debugging blind. Log it or rethrow it.
  • Log with the exception object. Pass the exception itself to your logger, not only its message, so the stack trace survives: logger.error("Update failed", e).
  • Preserve the cause when wrapping. Chain the original into the new exception so the root cause stays in the trace.
  • Clean up with try-with-resources. Let AutoCloseable handle closing rather than a hand-written finally.
  • Do not use exceptions for control flow. Throwing to break a loop is slower and harder to read than a plain condition.

Logging an exception correctly

Effective error reporting separates two audiences: the developer reading logs and the user seeing the screen. Give the developer the full trace, and the user a message they can act on.

try {
    databaseService.updateProfile(user);
} catch (SQLException e) {
    logger.error("Profile update failed for user " + user.getId(), e);
    showMessage("We could not save your changes. Please try again.");
}

Pass e as the second argument so the logging framework, whether Log4j 2 or SLF4J with Logback, records the full stack trace including any chained cause.

GeeksProgramming Java tutor helping a student debug an exception stack trace

Exceptions in a multithreaded environment

In a multithreaded program, an exception thrown in one thread does not propagate to the thread that started it; it ends only the thread where it occurred. A worker thread that dies silently can leave the rest of the application running on stale or partial data, which is why centralized handling matters.

Install a default handler with Thread.setDefaultUncaughtExceptionHandler so no thread fails without a trace, and prefer the ExecutorService framework, which captures exceptions inside the Future it returns from submit.

Thread.setDefaultUncaughtExceptionHandler((thread, throwable) -> {
    logger.error("Uncaught exception in thread " + thread.getName(), throwable);
});

With an ExecutorService, the exception surfaces when you call future.get(), wrapped in an ExecutionException whose cause is the original. For the threading model behind this, see our guide to Java concurrency and multithreading.

Real-world exception handling

Exception handling earns its place when it turns a crash into a controlled response a user can understand. Three patterns show up across web apps, mobile clients, and services.

Guarding a user action

When a user adds an out-of-stock item to a cart, catching the domain exception lets you show a clear message instead of a broken page.

public void addItemToCart(Item item) {
    try {
        shoppingCart.add(item);
    } catch (OutOfStockException e) {
        showMessage("Sorry, " + item.getName() + " is currently out of stock.");
    }
}

Handling a network call

A mobile request can fail because the connection dropped, not because the code is wrong. Catch the network exception and offer a retry rather than freezing the screen.

public void fetchUserData() {
    try {
        UserData data = networkService.fetchUserData();
        render(data);
    } catch (NetworkException e) {
        showMessage("Unable to reach the server. Check your connection and retry.");
    }
}

Wrapping a database failure

Database code throws low-level SQLException objects that mean nothing to a caller higher up. Catch them at the data layer and rethrow a domain exception that preserves the cause.

public void updateProfile(User user) {
    try {
        databaseService.updateUserProfile(user);
    } catch (SQLException e) {
        throw new ProfileUpdateException("Could not update profile " + user.getId(), e);
    }
}

Passing e as the second argument chains the original SQLException as the cause, so the trace shows both layers under a Caused by: line. The list you load out of that profile often lands in a Map or List, where the Java Collections Framework takes over.

Common mistakes and pitfalls

Five mistakes turn exception handling from a safety net into a source of hidden bugs. Each has a direct fix.

Swallowing the exception

An empty catch block is the most damaging pitfall. The program keeps running as though nothing failed, and the bug surfaces later, far from its cause.

// Wrong: the failure vanishes
try {
    riskyOperation();
} catch (IOException e) {
    // nothing here
}

At minimum, log the exception. Better, decide whether to recover or rethrow.

Catching too broadly

catch (Exception e) traps everything, including the NullPointerException and IllegalStateException bugs that should crash loud and get fixed. A broad catch belongs only at a top-level boundary where you log and return a safe response.

Losing the original cause

Rethrowing without chaining throws away the stack trace that points to the real problem. Always wrap with the cause.

// Wrong: trace points only to this line
throw new ServiceException("Load failed");

// Right: trace shows both layers
throw new ServiceException("Load failed", e);

Using exceptions for control flow

Throwing an exception to exit a loop or signal a found value is slower than a plain condition and obscures the logic. Reserve exceptions for actual error conditions.

Failing to close resources

A FileInputStream or Connection left open leaks a file handle or a pool slot until the resource pool runs dry. Try-with-resources closes them every time, even on the exception path, so use it for anything AutoCloseable.

Advanced patterns: circuit breaker and retry

In distributed systems, two patterns build on exception handling to keep a failing dependency from taking down the whole service. Both treat a repeated exception as a signal to change strategy rather than to keep trying blindly.

Circuit breaker

The circuit breaker pattern counts failures from a downstream call and, once they cross a threshold, stops sending requests for a cooldown period. It returns a fallback or a fast failure instead, which protects both the caller and the struggling service. Libraries like Resilience4j implement it so you wrap a call rather than hand-roll the state machine.

Retry and timeout

Retry logic reattempts a transient failure, such as a dropped connection, a fixed number of times with a growing delay between attempts, a technique called exponential backoff. A timeout caps how long any single attempt waits. Pair them: retry handles the blip, and the timeout stops a single slow call from blocking a thread forever.

Get help with your Java exception handling assignment

Exception handling rewards a few consistent habits: catch the most specific type, never swallow a failure, preserve the cause when you wrap, and close resources with try-with-resources. Start from the small examples here and the same patterns scale to web services, Android apps, and concurrent pipelines. When a deadline is close and an exception is crashing code you cannot trace, the developers behind our Java assignment help service write code to your Java version, return a walkthrough you can defend in a viva, and take 50% upfront with the other 50% due only after you verify it runs.

Frequently asked questions

What is the difference between checked and unchecked exceptions in Java?

Checked exceptions extend Exception (but not RuntimeException) and the compiler forces you to catch them or declare them with throws. IOException and SQLException are checked. Unchecked exceptions extend RuntimeException and need no declaration. NullPointerException, ArithmeticException, and ArrayIndexOutOfBoundsException are unchecked. Use checked exceptions for recoverable conditions outside your control, and unchecked exceptions for programming bugs.

Does the finally block always run in Java?

The finally block runs whether or not an exception is thrown, and even when the try or catch block executes a return. The two cases that skip it are a call to System.exit() inside the try or catch block, and the JVM crashing or the thread being killed. Use finally for cleanup that must happen, though try-with-resources is the better tool for closing resources.

What is the difference between throw and throws in Java?

throw is a statement that raises a single exception object at runtime, for example throw new IllegalArgumentException("id is null"). throws is part of a method signature that declares which checked exceptions the method can propagate, for example void read() throws IOException. throw acts; throws warns the caller.

How do I create a custom exception in Java?

Define a class that extends Exception for a checked exception or RuntimeException for an unchecked one, then add a constructor that calls super(message). Add a constructor that also takes a Throwable cause and passes it to super(message, cause) so the original stack trace is preserved. End the class name in Exception, for example InsufficientFundsException.

When should I use try-with-resources instead of finally?

Use try-with-resources for any object that implements AutoCloseable, such as streams, readers, JDBC connections, and sockets. It closes each resource automatically in reverse order, even when an exception is thrown, and it removes the nested null check and inner try-catch that a manual finally block needs. Reserve a plain finally block for cleanup that is not a closeable resource.

Is it bad to catch the generic Exception class in Java?

Catching Exception or Throwable is usually a mistake because it swallows bugs you did not anticipate, including NullPointerException and programming errors that should crash and be fixed. Catch the most specific exception type you can handle, and let everything else propagate. A broad catch is acceptable only at a top-level boundary, such as a request handler, where you log the error and return a safe response.

What happens if an exception is never caught in Java?

An uncaught exception travels up the call stack frame by frame. If no method catches it, the JVM passes it to the thread's default uncaught exception handler, prints the stack trace to standard error, and terminates that thread. In a single-threaded program that ends the application. You can install a custom handler with Thread.setDefaultUncaughtExceptionHandler to log the failure instead of losing it.

How do I rethrow an exception without losing the original cause?

Pass the caught exception as the second argument when you wrap it: throw new ServiceException("load failed", e). This chains the original as the cause, so the printed stack trace shows both the wrapper and the root cause under a Caused by: line. Never write throw new ServiceException("load failed") on its own inside a catch block, because that discards the stack trace that tells you where the failure started.

java exception handling try catch finally checked exceptions unchecked exceptions custom exceptions throw throws try-with-resources
Share: X / Twitter LinkedIn

Related articles

  • Java

    Advanced Java Data Management Techniques

    Master advanced Java data management: optimize data structures, handle concurrent access, tune memory, and use serialization and compression in real applications.

    May 3, 2024

  • Java

    Java File I/O: Read, Write, and Manage Files

    A practical guide to Java file I/O: streams, readers and writers, NIO Path and Files, buffering, serialization, and the exceptions that break file code.

    Oct 7, 2023

  • Java

    Sorting Algorithms in Java: Step-by-Step

    Bubble Sort and Quick Sort implemented in Java with time complexity analysis, step-by-step code walkthroughs, and working examples you can run immediately.

    Jun 23, 2022

← All articles

Stuck on a programming assignment?

Get expert help in Java, C++, Python, JavaScript, SQL, and more. We deliver working code with a clear walkthrough so you can understand and defend it.