Try/Catch/Finally and Exception Types

8 min read

When something goes wrong in Java — a missing file, a bad number, a DOM element that didn't appear — Java throws an exception. Exceptions aren't return values; they unwind the stack until something catches them or the program crashes. Test code lives at the rough edge: Selenium throws NoSuchElementException, JSON parsers throw JsonProcessingException, file I/O throws IOException. Knowing the exception model — try, catch, finally, checked vs unchecked — is the difference between a test framework that prints "test failed" and one that says "the dashboard timed out at line 42 of LoginPage.java."

Try / catch — the basic shape

public class ParseDemo {
    public static void main(String[] args) {
        try {
            int code = Integer.parseInt("abc");
            System.out.println("parsed: " + code);
        } catch (NumberFormatException e) {
            System.out.println("Not a number: " + e.getMessage());
        }
        System.out.println("done");
    }
}

Output:

Not a number: For input string: "abc"
done

Read the flow:

  1. The try block runs. Integer.parseInt("abc") fails and throws NumberFormatException.
  2. The JVM looks for a matching catch block. It finds one — catch (NumberFormatException e) — and jumps there. The e is the exception object, with a .getMessage() describing what happened.
  3. Execution continues after the try/catch. done prints.

If you don't catch an exception, it propagates up the call stack. Eventually it reaches main's caller (the JVM) and the program terminates with a stack trace.

Multiple catch blocks

You can react differently to different exception types:

import java.io.FileReader;
import java.io.IOException;
 
public class ReaderDemo {
    public static void main(String[] args) {
        try {
            FileReader reader = new FileReader("config.json");
            int b = reader.read();
            System.out.println(b);
        } catch (java.io.FileNotFoundException e) {
            System.out.println("Config missing — falling back to defaults");
        } catch (IOException e) {
            System.out.println("Read error: " + e.getMessage());
        }
    }
}

Java picks the first matching catch. The order matters when types are related: FileNotFoundException is a subclass of IOException, so the more specific type goes first. Reverse the order and the compiler complains: exception has already been caught.

A multi-catch alternative when you want to react identically to several types:

} catch (FileNotFoundException | NumberFormatException e) {
    System.out.println("Recoverable input problem: " + e.getMessage());
}

Finally — always runs

The finally block runs whether or not the try succeeded:

public class WithDriver {
    public static void main(String[] args) {
        Object driver = null;
        try {
            driver = "ChromeDriver";        // pretend new ChromeDriver()
            System.out.println("running tests on " + driver);
            // throw new RuntimeException("element not found");
        } catch (Exception e) {
            System.out.println("Test failed: " + e.getMessage());
        } finally {
            if (driver != null) {
                System.out.println("closing " + driver);
            }
        }
    }
}

Output:

running tests on ChromeDriver
closing ChromeDriver

Uncomment the throw and you'll see:

running tests on ChromeDriver
Test failed: element not found
closing ChromeDriver

Either way, finally runs — even if the try returns early or the catch re-throws. That guarantee is what makes finally the standard place for cleanup: closing browsers, releasing database connections, deleting temp files.

For resources that implement AutoCloseable, try-with-resources is even nicer:

try (FileReader reader = new FileReader("config.json")) {
    // use reader
} catch (IOException e) {
    System.out.println("read failed: " + e.getMessage());
}
// reader is closed automatically — no finally needed

Lesson 3 of this chapter uses try-with-resources for files; it's the modern shape. Reach for explicit finally only when the cleanup isn't a simple .close().

The exception hierarchy

Every exception is a class. Three layers worth knowing:

  • Throwable — the root. Anything throwable in Java extends this.
  • Error — serious JVM problems (OutOfMemoryError, StackOverflowError). Don't catch these. They mean the JVM is in a bad place; the right response is to crash, not soldier on.
  • Exception — the everyday ones. Two flavours:
    • Checked exceptions (e.g. IOException, SQLException, InterruptedException). The compiler forces you to handle them — either with a try/catch or by declaring throws SomeException on the enclosing method. This is Java's signature feature: forgetting an error-handling path is a compile error, not a runtime crash.
    • Unchecked exceptions — anything that extends RuntimeException (NullPointerException, IllegalArgumentException, ArrayIndexOutOfBoundsException, NumberFormatException). The compiler doesn't force you to catch them; they're for programming bugs that shouldn't happen.

The mental model: checked = "this might fail for reasons outside your control (file gone, network down)"; unchecked = "this is a bug in your code (null reference, index out of range)."

Common exceptions in QA code

A field guide to the ones you'll see:

  • NullPointerException — calling a method or accessing a field on a null reference. The most common bug. Fix: add a null check or use Optional.
  • ArrayIndexOutOfBoundsExceptionarr[i] where i is out of range. Fix: bound the loop properly.
  • NumberFormatExceptionInteger.parseInt("abc"). Fix: validate input or catch and react.
  • ClassCastException(String) someObject where someObject isn't a String. Fix: use instanceof first.
  • NoSuchElementException (Selenium) — element not in the DOM. Fix: explicit wait, retry, or test that the precondition was set up.
  • TimeoutException (Selenium) — wait condition didn't satisfy in time. Fix: increase timeout, fix flake, or check the wait condition.
  • StaleElementReferenceException (Selenium) — the DOM rebuilt and your reference is stale. Fix: re-find the element after the rebuild.
  • IOException — file/network failure. Checked; you must catch or declare.
  • IllegalArgumentException — code refused an argument (often your validation code, from chapter 4.3).

Selenium throws all of these and more. A Selenium-aware test framework usually catches WebDriverException (the supertype) at the framework boundary and turns it into a structured failure with screenshot, page source, and timing.

A real QA wrapper

Wrapping a Selenium-style call so a missing element doesn't crash the suite:

public class SafeFinder {
 
    static class WebElement {
        String text;
        WebElement(String text) { this.text = text; }
    }
 
    static class NoSuchElementException extends RuntimeException {
        public NoSuchElementException(String msg) { super(msg); }
    }
 
    static WebElement realFind(String selector) {
        if (selector.equals("#missing")) throw new NoSuchElementException("not found: " + selector);
        return new WebElement("Hello, " + selector);
    }
 
    public static WebElement findOrNull(String selector) {
        try {
            return realFind(selector);
        } catch (NoSuchElementException e) {
            System.out.println("[warn] " + e.getMessage());
            return null;
        }
    }
 
    public static void main(String[] args) {
        WebElement ok = findOrNull("#submit");
        WebElement gone = findOrNull("#missing");
 
        System.out.println("found ok? " + (ok != null));
        System.out.println("found gone? " + (gone != null));
    }
}

Output:

[warn] not found: #missing
found ok? true
found gone? false

findOrNull contains the exception — its callers see a simple "got it" / "didn't get it" boolean, not a stack trace. That kind of thin wrapper around throwing APIs is the everyday work of building a test framework on top of Selenium or Rest Assured.

try / catch / finally — the flow

Two things stand out: every path leads to finally, and a non-matching exception still flows through finally before propagating upward. That's why finally is the right place for cleanup that must happen.

⚠️ Common mistakes

  • catch (Exception e) { } — the empty catch. Swallowing every exception silently is one of the most damaging patterns in Java. The bug becomes invisible; tests "pass" while the system is broken. At minimum, log the exception. Better, catch only what you can actually handle.
  • Catching too broad a type. catch (Exception) matches NullPointerException you didn't expect. Catch the specific types you can recover from and let the rest propagate. A test framework that turns all failures into "unknown error" is debugging hell.
  • Re-throwing while losing the cause. throw new RuntimeException("boom"); inside a catch loses the original exception's stack trace. Use the wrapping constructor: throw new RuntimeException("boom", e); so the cause chain is preserved. Future-you reading the test report will thank you.

🎯 Practice task

Wrap a flaky operation. 25-30 minutes.

  1. Create FlakyParse.java.
  2. Write a method static int parseStatus(String s) that returns Integer.parseInt(s) but catches NumberFormatException and returns -1 instead. Log the offending input.
  3. In main, build String[] codes = {"200", "abc", "404", "", "500"}; and walk it. For each value, call parseStatus and print the result. Confirm "abc" and "" produce -1 with a log line, while the others produce the right number.
  4. Add a finally to parseStatus that prints "checked: <s>" regardless of outcome. Confirm it runs for both successes and failures.
  5. Add a method static void readConfig(String path) throws IOException (we'll meet throws formally next lesson). Inside, simulate an IOException with if (path == null) throw new IOException("null path");. In main, call it inside a try/catch and react.
  6. Stretch: chain wrapping — write a method that catches IOException and re-throws RuntimeException("config load failed", e). Catch the runtime exception in main and call e.getCause() to get back the original IOException. That cause chain is what gives a useful stack trace at the top of a real test failure.

You can now react to errors instead of crashing on them. Lesson 2 covers throw/throws and the custom exception classes that make test failures self-describing.

// tip to track lessons you complete and pick up where you left off across devices.