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:
- The
tryblock runs.Integer.parseInt("abc")fails and throwsNumberFormatException. - The JVM looks for a matching
catchblock. It finds one —catch (NumberFormatException e)— and jumps there. Theeis the exception object, with a.getMessage()describing what happened. - Execution continues after the
try/catch.doneprints.
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 neededLesson 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 atry/catchor by declaringthrows SomeExceptionon 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.
- Checked exceptions (e.g.
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 anullreference. The most common bug. Fix: add a null check or useOptional.ArrayIndexOutOfBoundsException—arr[i]whereiis out of range. Fix: bound the loop properly.NumberFormatException—Integer.parseInt("abc"). Fix: validate input or catch and react.ClassCastException—(String) someObjectwheresomeObjectisn't a String. Fix: useinstanceoffirst.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)matchesNullPointerExceptionyou 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.
- Create
FlakyParse.java. - Write a method
static int parseStatus(String s)that returnsInteger.parseInt(s)but catchesNumberFormatExceptionand returns-1instead. Log the offending input. - In
main, buildString[] codes = {"200", "abc", "404", "", "500"};and walk it. For each value, callparseStatusand print the result. Confirm"abc"and""produce-1with a log line, while the others produce the right number. - Add a
finallytoparseStatusthat prints"checked: <s>"regardless of outcome. Confirm it runs for both successes and failures. - Add a method
static void readConfig(String path) throws IOException(we'll meetthrowsformally next lesson). Inside, simulate an IOException withif (path == null) throw new IOException("null path");. Inmain, call it inside a try/catch and react. - Stretch: chain wrapping — write a method that catches
IOExceptionand re-throwsRuntimeException("config load failed", e). Catch the runtime exception inmainand calle.getCause()to get back the originalIOException. 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.