07 - Exceptions¶
What this session is¶
About an hour. You'll learn how Java handles things going wrong - files that don't exist, numbers that can't be parsed, division by zero. Java uses exceptions that fly up the call stack until caught. You'll also meet checked vs unchecked exceptions (Java's controversial design choice) and try-with-resources, the modern way to handle cleanup.
A small example¶
public class Boom {
public static void main(String[] args) {
int n = Integer.parseInt("hello"); // boom
System.out.println(n);
}
}
Compile and run. You'll see:
Exception in thread "main" java.lang.NumberFormatException: For input string: "hello"
at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
at java.base/java.lang.Integer.parseInt(Integer.java:661)
at java.base/java.lang.Integer.parseInt(Integer.java:777)
at Boom.main(Boom.java:3)
NumberFormatException is the type of exception. The message says what happened. The stack trace below shows the call chain. Read it bottom-up - the last line (Boom.main(Boom.java:3)) is where the chain started.
If nothing catches the exception, the program crashes.
Catching: try/catch¶
public class SafeParse {
public static void main(String[] args) {
try {
int n = Integer.parseInt("hello");
System.out.println("got " + n);
} catch (NumberFormatException e) {
System.out.println("not a number: " + e.getMessage());
}
}
}
How it reads:
- Try the code in the try block.
- If it throws NumberFormatException, jump to the catch block. e is the exception object.
- If no exception, skip the catch.
e.getMessage() gets the human-readable message. e.printStackTrace() dumps the stack trace (useful for debugging; avoid in production - log the exception properly instead).
Catching multiple exception types¶
Multiple catch clauses (first match wins, top to bottom):
try {
risky();
} catch (NumberFormatException e) {
handleBadNumber(e);
} catch (IOException e) {
handleIoProblem(e);
} catch (Exception e) {
System.out.println("something else went wrong: " + e);
}
Multi-catch (Java 7+) - same handling for several types:
finally: code that always runs¶
try {
doSomething();
} catch (Exception e) {
handle(e);
} finally {
cleanup(); // runs whether or not an exception occurred
}
finally is for cleanup that must happen no matter what - closing files, releasing locks, restoring state. Use sparingly; the modern try-with-resources (below) usually replaces it.
try-with-resources: automatic cleanup¶
For anything that needs to be closed after use (files, network sockets, database connections), use try-with-resources:
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class ReadFile {
public static void main(String[] args) {
try (BufferedReader reader = new BufferedReader(new FileReader("notes.txt"))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
System.out.println("couldn't read: " + e.getMessage());
}
}
}
The resource declared in the try (...) parens is automatically closed when the block exits - whether normally or via exception. No finally needed.
Multiple resources, semicolon-separated:
Any class implementing AutoCloseable works with try-with-resources. Almost all standard I/O classes do.
Use try-with-resources for everything that needs closing. It's safer than manual close + finally and reads cleaner.
Checked vs unchecked: Java's controversial design¶
Java has two families of exceptions:
- Checked exceptions - extend
Exceptionbut notRuntimeException. The compiler forces you to handle them (or declare you throw them). Examples:IOException,SQLException. - Unchecked exceptions - extend
RuntimeException. No compiler enforcement. Examples:NullPointerException,IllegalArgumentException,NumberFormatException,IndexOutOfBoundsException.
// Unchecked - compiler doesn't force anything:
public int parseAge(String s) {
return Integer.parseInt(s); // can throw NumberFormatException; no compile error
}
// Checked - compiler forces declaration:
public String readConfig(String path) throws IOException {
return Files.readString(Path.of(path)); // throws IOException; must declare
}
To call a method that throws a checked exception, you must either:
1. Catch it.
2. Declare your own method throws the same exception, passing the obligation upward.
public String readConfig(String path) throws IOException {
return Files.readString(Path.of(path));
}
public void useConfig() {
try {
String text = readConfig("conf.json");
// use text
} catch (IOException e) {
System.err.println("config unreadable: " + e.getMessage());
}
}
The debate: checked exceptions force you to think about failures (good!) but also create unwanted noise when you don't have a sensible recovery (bad!). Modern Java practice:
- Use unchecked exceptions for almost everything in your own code. Extend
RuntimeExceptionfor custom errors. - Tolerate checked exceptions when consuming standard-library APIs that use them (
IOException,SQLException). - Don't propagate checked exceptions up through many layers. Wrap them as unchecked at the boundary.
Raising your own exceptions¶
public void withdraw(double amount) {
if (amount <= 0) {
throw new IllegalArgumentException("amount must be positive: " + amount);
}
if (amount > balance) {
throw new IllegalStateException("insufficient funds");
}
balance -= amount;
}
Use existing exception types when they fit:
- IllegalArgumentException - bad argument value.
- IllegalStateException - operation not legal in current state.
- NullPointerException - usually let Java throw this for you.
- UnsupportedOperationException - method not implemented or not allowed.
Define your own when callers should catch this specific kind of failure:
public class InsufficientFundsException extends RuntimeException {
public InsufficientFundsException(String message) {
super(message);
}
}
Extending RuntimeException (not Exception) keeps it unchecked.
Common exceptions you'll meet¶
| Exception | Meaning |
|---|---|
NullPointerException (NPE) |
called a method on null |
ArrayIndexOutOfBoundsException |
array index out of range |
IndexOutOfBoundsException |
similar, for List.get(n) etc. |
ClassCastException |
tried to cast to wrong type |
NumberFormatException |
Integer.parseInt got bad text |
IllegalArgumentException |
bad argument |
IllegalStateException |
wrong state for the operation |
IOException |
I/O failed (checked) |
RuntimeException |
base of all unchecked |
Exception |
base of all (use sparingly) |
The classic NPE¶
NullPointerException is Java's most-shipped bug. It happens when you call a method on a null reference:
Modern fixes:
- Objects.requireNonNull(s, "s") - fail loudly with a useful message if null is passed.
- Optional<String> - explicit "value may or may not be there" type.
- @NonNull annotations (JSR 305, Checker Framework, IntelliJ inspections) - static checks that catch potential NPEs at compile time.
Java doesn't have null-safety baked in (like Kotlin does). The discipline: be explicit about which references can be null and check at boundaries.
Exercise¶
In a new file Parse.java:
- Write a method
parsePositive(String s)that returns anint: - Parse
sas an integer usingInteger.parseInt. - If parsing fails, catch the
NumberFormatExceptionand throw a newIllegalArgumentExceptionwith a useful message like"not a number: " + s. - If the parsed number is ≤ 0, throw
IllegalArgumentExceptionwith"must be positive, got " + n. -
Otherwise return the number.
-
In
Use try-catch around each call tomain, loop over these inputs and print success or error for each:parsePositive. Format:42 -> 42for success,hello -> error: not a number: hellofor failure. -
Stretch: create your own
BadInputException extends RuntimeException. HaveparsePositivethrow it instead ofIllegalArgumentException. Updatemain'scatchaccordingly.
What you might wonder¶
"When should I make my exception checked vs unchecked?" Modern advice: unchecked, unless you have a very strong reason. Checked forces callers to handle, which sounds good but in practice produces "swallow the exception" anti-patterns. The standard library still uses checked exceptions (IOException etc.) for historical reasons; you'll have to deal with them.
"What's the difference between throw and throws?"
throw (verb) raises an exception: throw new RuntimeException("bad");. throws (clause) declares that a method might throw a checked exception: void foo() throws IOException { ... }. Easy to confuse; read aloud as "I throw" vs "I throws".
"Should I ever catch Throwable or Error?"
No. Throwable is the root of both Exception and Error. Error (OutOfMemoryError, StackOverflowError) means "the JVM is in trouble" - there's nothing useful you can do; let it propagate and crash. Catch Exception at most.
"What about try without catch but with finally?"
Legal: try { ... } finally { ... }. Useful when you want cleanup but can't actually handle the exception meaningfully. Almost always replaced by try-with-resources in modern code.
Done¶
You can now:
- Read stack traces (bottom-up).
- Catch exceptions with try/catch.
- Use multi-catch and finally.
- Use try-with-resources for automatic cleanup.
- Distinguish checked from unchecked exceptions.
- Raise your own exceptions.
- Define custom exception types.
Next page: modern Java's superpowers - records, sealed classes, pattern matching.
Next: Records, sealed, pattern matching →