By the end of this module, you will be able to:
Throwable → Exception → RuntimeExceptiontry-catch blocks to handle exceptions gracefullyfinally to guarantee cleanup code always runstry-with-resources for automatic resource managementthrowthrowsAn exception is an event that disrupts the normal flow of a program at runtime. When something goes wrong — dividing by zero, accessing a null reference, reading a file that doesn't exist — Java creates an exception object that describes the problem and immediately stops executing the current code path.
Without any handling, an exception crashes the program and prints a stack trace — the chain of method calls that led to the problem:
public class Main {
public static void main(String[] args) {
int result = divide(10, 0);
System.out.println(result); // never reached
}
public static int divide(int a, int b) {
return a / b;
}
}
Exception in thread "main" java.lang.ArithmeticException: / by zero
at Main.divide(Main.java:7)
at Main.main(Main.java:3)
Reading a stack trace: start from the top (where the exception occurred) and work down (how it got there). Main.divide line 7 is where division by zero happened; Main.main line 3 is what called it.
🔍 JavaScript has
try/catchtoo, and exceptions work the same way conceptually. The key difference is that Java distinguishes between exceptions the compiler forces you to handle and those it doesn't — a distinction JavaScript doesn't make.
Every exception in Java is an object. They all sit in a class hierarchy rooted at Throwable:
Throwable
├── Error ← JVM-level problems, do NOT catch these
│ ├── OutOfMemoryError
│ ├── StackOverflowError
│ └── ...
└── Exception ← Problems your program should handle
├── IOException ← Checked
├── SQLException ← Checked
├── FileNotFoundException ← Checked
└── RuntimeException ← Unchecked
├── NullPointerException
├── ArithmeticException
├── ArrayIndexOutOfBoundsException
├── IllegalArgumentException
├── IllegalStateException
└── ...
Error — represents serious JVM-level failures your program cannot recover from (OutOfMemoryError, StackOverflowError). Never catch these — if the JVM runs out of memory, there is nothing your application can do.
Exception — the branch your code deals with. Split into two families with very different rules.
This is the most important distinction in Java exception handling.
RuntimeException and subclasses)These indicate programming errors — bugs in your logic that should be fixed, not caught. The compiler does not require you to handle them.
String name = null;
name.length(); // NullPointerException — you should have checked for null
int[] arr = new int[3];
arr[5] = 10; // ArrayIndexOutOfBoundsException — index out of range
int x = 10 / 0; // ArithmeticException — divide by zero
The right response to unchecked exceptions is usually to fix the code, not wrap it in a try-catch.
Exception subclasses excluding RuntimeException)These represent external conditions outside your control — a file that doesn't exist, a network that drops, a database that's unavailable. The compiler forces you to handle them. If you don't, the code won't compile.
// Reading a file — FileNotFoundException is checked
public void readFile(String path) {
FileReader reader = new FileReader(path); // ❌ compile error: unhandled exception
}
// You must either handle it...
public void readFile(String path) {
try {
FileReader reader = new FileReader(path);
} catch (FileNotFoundException e) {
System.out.println("File not found: " + path);
}
}
// ...or declare that you pass it up the call stack
public void readFile(String path) throws FileNotFoundException {
FileReader reader = new FileReader(path);
}
| Unchecked | Checked | |
|---|---|---|
| Extends | RuntimeException |
Exception (not via RuntimeException) |
| Compiler enforcement | ❌ None | ✅ Must handle or declare |
| Represents | Programming bugs | External failures |
| Correct response | Fix the code | Handle gracefully |
| Examples | NullPointerException, ArithmeticException |
IOException, SQLException |
try-catch BlocksWrap risky code in a try block. If an exception occurs, execution jumps immediately to the matching catch block. Code after the throw point inside try is skipped.
try {
int result = divide(10, 0);
System.out.println(result); // skipped if exception occurs above
} catch (ArithmeticException e) {
System.out.println("Cannot divide by zero: " + e.getMessage());
}
// program continues here normally
Catch each exception type separately to handle them differently:
try {
String input = getUserInput();
int number = Integer.parseInt(input); // NumberFormatException if not a number
int result = 100 / number; // ArithmeticException if zero
System.out.println("Result: " + result);
} catch (NumberFormatException e) {
System.out.println("Please enter a valid number.");
} catch (ArithmeticException e) {
System.out.println("Number cannot be zero.");
}
When two exceptions need the same handling, combine them with |:
try {
riskyOperation();
} catch (IOException | SQLException e) {
System.out.println("Data operation failed: " + e.getMessage());
}
Exception VariableInside the catch block, e is the exception object. Use it to get information about what went wrong:
catch (IOException e) {
System.out.println(e.getMessage()); // human-readable description
e.printStackTrace(); // full stack trace — useful for debugging
}
⚠️
e.printStackTrace()is useful during development but should not be the sole error handling in production code. Always log or communicate the error meaningfully.
finally BlockCode inside finally always runs — whether an exception was thrown or not, whether it was caught or not. This makes it the right place for cleanup code that must always execute.
Scanner scanner = new Scanner(System.in);
try {
System.out.print("Enter a number: ");
int number = scanner.nextInt();
System.out.println("Square: " + (number * number));
} catch (InputMismatchException e) {
System.out.println("That was not a valid number.");
} finally {
scanner.close(); // always runs — resource is always cleaned up
System.out.println("Scanner closed.");
}
finally runs in all three scenarios:
| Scenario | try body |
catch body |
finally body |
|---|---|---|---|
| No exception | ✅ Runs fully | ❌ Skipped | ✅ Runs |
| Exception caught | ⚠️ Stops at throw | ✅ Runs | ✅ Runs |
| Exception not caught | ⚠️ Stops at throw | ❌ Skipped | ✅ Runs before crash |
💡 The main use cases for
finallyare closing file handles, releasing database connections, and unlocking resources. In modern Java,try-with-resources(next section) handles most of these more cleanly.
try-with-resources — Automatic Resource Cleanuptry-with-resources is a cleaner syntax for managing resources that must be closed after use — files, database connections, network streams. Any resource that implements the AutoCloseable interface can be used here.
Declare the resource inside the parentheses after try. Java automatically calls .close() on it when the block exits — even if an exception is thrown.
// Without try-with-resources — verbose, easy to forget close()
FileReader reader = null;
try {
reader = new FileReader("data.txt");
// ... read file
} catch (IOException e) {
System.out.println("Error: " + e.getMessage());
} finally {
if (reader != null) {
try { reader.close(); } catch (IOException e) { /* ignore */ }
}
}
// With try-with-resources — clean, automatic
try (FileReader reader = new FileReader("data.txt")) {
// ... read file
// reader.close() called automatically when block exits
} catch (IOException e) {
System.out.println("Error: " + e.getMessage());
}
Multiple resources can be declared separated by a semicolon:
try (FileReader reader = new FileReader("input.txt");
FileWriter writer = new FileWriter("output.txt")) {
// both closed automatically
} catch (IOException e) {
System.out.println("File operation failed: " + e.getMessage());
}
💡 You will use
try-with-resourcesextensively when working with databases and file I/O in later weeks. For now, understand the pattern and why it exists — it eliminates an entire class of resource-leak bugs.
throw — Throwing Exceptions ExplicitlyYou can throw an exception yourself using the throw keyword. This is how you signal to the caller that something has gone wrong — typically when input validation fails or a precondition is violated.
public static double calculateSquareRoot(double number) {
if (number < 0) {
throw new IllegalArgumentException(
"Cannot calculate square root of a negative number: " + number
);
}
return Math.sqrt(number);
}
System.out.println(calculateSquareRoot(25.0)); // 5.0
System.out.println(calculateSquareRoot(-4.0)); // throws IllegalArgumentException
💡 Always throw the most specific exception type that fits the situation.
IllegalArgumentExceptionis for invalid input values.IllegalStateExceptionis for calling a method when the object is in the wrong state.NullPointerExceptioncan be thrown explicitly when a required argument is null — though the Objects utility class hasObjects.requireNonNull()for this.
throws — Declaring Exceptions in Method SignaturesWhen a method can throw a checked exception but doesn't handle it internally, it must declare this in its signature using throws. This tells callers: "if you call this method, you must deal with this exception."