Exception handling: try, catch, finally, throw, throws
Where control goes when an error happens, when finally runs, the checked vs unchecked difference, and writing your own exception class. The favorite of output-tracing questions.
It is easy to write what a program does when everything is fine. Exception handling organizes what happens when things go wrong: the user types a letter instead of a number, the file does not exist, an array bound is exceeded. In the exam this topic almost always comes as a “What is printed?” tracing question: which lines are skipped, which catch runs, where finally steps in. Alongside it come compile errors about checked exceptions and the “write your own exception class” question.
Golden rules:
- The moment an exception is thrown, the rest of the try block is skipped. Control goes to the matching catch.
- Catches are tried from top to bottom, and the first one that fits runs. What fits is the exception’s own type or its superclass.
- finally always runs: with or without an exception, even with a return.
- An uncaught exception ends the method immediately and passes to the caller (stack unwinding).
- A checked exception is either caught or declared with
throws. There is no such requirement for unchecked ones (RuntimeException).
try, catch, finally
try {
int[] arr = {1, 2, 3};
System.out.println(arr[1]);
System.out.println(arr[3]); // the exception happens here
System.out.println("not printed"); // skipped
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("Caught: " + e.getMessage());
} finally {
System.out.println("finally");
}
System.out.println("after");
2
Caught: Index 3 out of bounds for length 3
finally
after
Rules:
- A try cannot stand alone: it needs at least one catch or one finally. Otherwise
'try' without 'catch', 'finally' or resource declarations. - A try can have more than one catch; only one of them runs.
- finally is optional; if present, it runs once at the very end. It is used for closing resources (files, connections).
Multiple catches and their order
static void trace(int k) {
try {
System.out.print("A");
if (k == 1) throw new IllegalStateException();
if (k == 2) throw new IllegalArgumentException();
System.out.print("B");
} catch (IllegalStateException e) {
System.out.print("C");
} catch (RuntimeException e) { // superclass: catches every RuntimeException
System.out.print("D");
} finally {
System.out.print("E");
}
System.out.println("F");
}
trace(0);
trace(1);
trace(2);
ABEF
ACEF
ADEF
If you write a general catch (Exception, RuntimeException) before specific ones, the catch below it never gets its turn and the compiler counts this as an error: exception NumberFormatException has already been caught.
Multi-catch: to combine catches that do the same job, write catch (NumberFormatException | ArithmeticException e). The list cannot contain types that are subclasses of each other.
static void parse(String s) {
try {
int n = Integer.parseInt(s);
System.out.println(100 / n);
} catch (NumberFormatException | ArithmeticException e) {
System.out.println("bad input: " + e.getClass().getSimpleName());
}
}
parse("4"); parse("x"); parse("0");
25
bad input: NumberFormatException
bad input: ArithmeticException
finally and return
static int f() {
try { return 1; }
finally { System.out.println("finally in f"); }
}
static int h() {
int x = 10;
try { return x; } // the return value was stored as 10
finally { x = 20; } // does not affect the stored value
}
System.out.println(f());
System.out.println(h());
finally in f
1
10
PITFALL · return inside finally. If you write
return 2;in finally, even an exception thrown from the try is swallowed and the method returns 2 (in the test run, a try withthrow new RuntimeExceptionand a finally withreturn 2silently returned 2). The compiler does not reject it (it only warns when-Xlintis on); never write it like this, but if you see it in the exam, the answer is the value in finally.
The exception hierarchy: checked and unchecked
Throwable
├─ Error (JVM problems: OutOfMemoryError, StackOverflowError; not caught)
└─ Exception (CHECKED: the compiler checks)
├─ IOException
│ └─ FileNotFoundException
├─ (your own extends Exception classes)
└─ RuntimeException (UNCHECKED: the compiler stays out)
├─ ArithmeticException
├─ NullPointerException
├─ ClassCastException
├─ IndexOutOfBoundsException
│ ├─ ArrayIndexOutOfBoundsException
│ └─ StringIndexOutOfBoundsException
└─ IllegalArgumentException
└─ NumberFormatException
| Checked | Unchecked | |
|---|---|---|
| Derives from? | Exception (except RuntimeException) | RuntimeException |
| What does the compiler want? | Either catch or throws | Nothing |
| When does it happen? | Situations outside the program’s control: file missing, network down | Programming errors: null, division by zero, wrong index |
| Example | IOException, FileNotFoundException | ArithmeticException, NullPointerException |
Common unchecked exceptions and the one-liners that produce them:
String s = null; s.length(); // NullPointerException
Object o = "hi"; Integer i = (Integer) o; // ClassCastException
System.out.println(10 % 0); // ArithmeticException: / by zero
System.out.println(10.0 / 0); // NO exception: Infinity
NullPointerException
ClassCastException
ArithmeticException: / by zero
Infinity
(The output of the first three lines was obtained by running each in a try/catch and printing the name of the caught exception.)
throw and throws
throwthrows an exception. An object follows it:throw new IllegalArgumentException("amount must be positive");throwsin a method’s signature declares which checked exceptions this method may let out:void withdraw(double a) throws InsufficientFundsException.
If you call a method that throws or may throw a checked exception, you have two options: catch it with try/catch, or add throws to your own signature and pass the responsibility up. If you do neither: unreported exception X; must be caught or declared to be thrown.
There is also a rule in the opposite direction: if nothing in the try block can throw a checked exception, you cannot catch it: exception IOException is never thrown in body of corresponding try statement.
Stack unwinding
If an exception happens in a method and is not caught there, the method ends at that moment. The exception behaves as if it were “thrown” to the place that called the method. The call stack is unwound this way until a catch that catches it is found. Every finally on the way runs.
static void level3() {
System.out.println("level3 start");
int[] a = new int[2];
a[5] = 1; // exception
System.out.println("level3 end");
}
static void level2() {
System.out.println("level2 start");
try { level3(); }
finally { System.out.println("level2 finally"); }
System.out.println("level2 end");
}
static void level1() {
System.out.println("level1 start");
try { level2(); }
catch (ArrayIndexOutOfBoundsException e) {
System.out.println("level1 caught: " + e.getMessage());
}
System.out.println("level1 end");
}
level1();
level1 start
level2 start
level3 start
level2 finally
level1 caught: Index 5 out of bounds for length 2
level1 end
main → level1 → level2 → level3 (call stack)
level3: a[5] → exception, no catch → level3 ends (no "end")
level2: no catch, finally EXISTS → "level2 finally" → exception continues (no "end")
level1: the catch fits → "level1 caught..." → normal flow → "level1 end"
If the exception goes all the way up to main and is not caught there either, the program ends and the JVM prints the stack trace. When level3() is called directly from main:
level3 start
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 5 out of bounds for length 2
at W09.level3(W09.java:31)
at W09.main(W09.java:221)
A stack trace is read from the bottom up: main called level3; the exception happened at line 31 of level3.
Your own exception class
To report business-rule violations (insufficient balance, invalid grade) with a meaningful type, you write your own exception class. Derive from Exception if you want it to be checked, from RuntimeException if you want it unchecked. Pass the message with super(message).
class InsufficientFundsException extends Exception {
private final double shortfall;
public InsufficientFundsException(String message, double shortfall) {
super(message);
this.shortfall = shortfall;
}
public double getShortfall() { return shortfall; }
}
class Account {
private double balance;
Account(double b) { balance = b; }
void withdraw(double amount) throws InsufficientFundsException {
if (amount <= 0) throw new IllegalArgumentException("amount must be positive");
if (amount > balance)
throw new InsufficientFundsException("Not enough money", amount - balance);
balance -= amount;
}
double getBalance() { return balance; }
}
Account acc = new Account(100);
try {
acc.withdraw(30);
acc.withdraw(100);
System.out.println("not printed");
} catch (InsufficientFundsException e) {
System.out.println(e.getMessage() + ", short by " + e.getShortfall());
}
System.out.println(acc.getBalance());
try {
acc.withdraw(-5);
} catch (InsufficientFundsException e) {
System.out.println("checked");
} catch (IllegalArgumentException e) {
System.out.println("unchecked: " + e.getMessage());
}
Not enough money, short by 30.0
70.0
unchecked: amount must be positive
Methods of the exception object
| Method | What does it give? | Example |
|---|---|---|
getMessage() | Only the message | bad value |
toString() | Class name + message | java.lang.IllegalArgumentException: bad value |
printStackTrace() | Prints the stack trace to stderr | the stack trace above |
getCause() | The exception that led to this one | chaining |
Catching an exception and wrapping it in a more meaningful one is called exception chaining: throw new IOException("load failed", e);. Then e.getCause().getMessage() gives the real reason (data.txt).
Common mistakes
| Wrong idea | The truth |
|---|---|
| After an exception, the rest of the try also runs | It is skipped, control goes to the catch |
| More than one catch can run | Only the first one that fits |
catch (Exception e) at the top | The ones below become unreachable, compile error |
| If there is a return, finally does not run | It runs, before the return |
| Changing a variable in finally changes the return | The return value was stored earlier |
| NumberFormatException is checked | It is from the RuntimeException family, unchecked |
throws throws the exception | throw throws, throws declares |
| A checked exception can be called without catching | catch or throws is required |
10.0 / 0 is an ArithmeticException | In double it is Infinity; only int division throws |
| After a caught exception the program ends | After the catch, normal flow continues |
Test yourself
Exam-style questions. Solve them on paper first, then reveal the answer.
What is printed?
try {
int[] arr = {1, 2, 3};
System.out.println(arr[1]);
System.out.println(arr[3]);
System.out.println("not printed");
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("Caught: " + e.getMessage());
} finally {
System.out.println("finally");
}
System.out.println("after");Show answer
arr[1] = 2 is printed. arr[3] throws an exception: the REST of the try is SKIPPED and the matching catch runs. finally runs in every case. Because the exception was caught, the program continues normally and "after" is printed.
What is printed by trace(0); trace(1); trace(2);?
static void trace(int k) {
try {
System.out.print("A");
if (k == 1) throw new IllegalStateException();
if (k == 2) throw new IllegalArgumentException();
System.out.print("B");
} catch (IllegalStateException e) {
System.out.print("C");
} catch (RuntimeException e) {
System.out.print("D");
} finally {
System.out.print("E");
}
System.out.println("F");
}Show answer
k = 0: no exception, A B, the catches are skipped, finally E, then F. k = 1: A, IllegalStateException fits the first catch: C, E, F. k = 2: A, IllegalArgumentException does not fit the first catch, the second catch RuntimeException is its superclass: D, E, F. Catches are tried from top to bottom, and the first one that fits runs.
What is printed by level1();?
static void level3() {
System.out.println("level3 start");
int[] a = new int[2];
a[5] = 1;
System.out.println("level3 end");
}
static void level2() {
System.out.println("level2 start");
try { level3(); }
finally { System.out.println("level2 finally"); }
System.out.println("level2 end");
}
static void level1() {
System.out.println("level1 start");
try { level2(); }
catch (ArrayIndexOutOfBoundsException e) {
System.out.println("level1 caught: " + e.getMessage());
}
System.out.println("level1 end");
}Show answer
Stack unwinding: the exception happens in level3, there is no catch there, and the method ends immediately ("level3 end" is not printed). In level2 there is no catch but there is a finally: finally runs first, then the exception continues upward ("level2 end" is not printed). In level1 it is caught, the message is printed, and level1 continues normally.
What do f() and h() return, and what is printed?
static int f() {
try { return 1; }
finally { System.out.println("finally in f"); }
}
static int h() {
int x = 10;
try { return x; }
finally { x = 20; }
}
// in main:
System.out.println(f());
System.out.println(h());Show answer
finally runs even before the return: first "finally in f", then the returned 1 is printed. In h, the return x expression computes and stores the value (10) at that moment; x = 20 in finally does not change the stored return value: 10. (If you wrote your own return inside finally, it would win, which is a bad habit.)
Find the compile error.
static void open(String name) {
throw new FileNotFoundException(name);
}Show answer
FileNotFoundException is a subclass of IOException, so it is checked. javac: unreported exception FileNotFoundException; must be caught or declared to be thrown. Fix: add throws FileNotFoundException to the signature or wrap it in try/catch.
Find the compile error.
try {
int x = Integer.parseInt("12");
} catch (Exception e) {
System.out.println("general");
} catch (NumberFormatException e) {
System.out.println("number");
}Show answer
Because Exception catches everything, the second catch never gets its turn. javac: exception NumberFormatException has already been caught. The rule: specific (subclass) catches first, general ones after.
Find the compile error.
try {
System.out.println("hi");
} catch (IOException e) {
System.out.println("io");
}Show answer
You cannot catch a checked exception if nothing in the try can throw it. javac: exception IOException is never thrown in body of corresponding try statement. This rule does not apply to unchecked exceptions and to Exception: catch (Exception e) would compile here.
Which exceptions are checked? (1) ArithmeticException (2) IOException (3) NullPointerException (4) FileNotFoundException (5) NumberFormatException
Show answer
Those derived from RuntimeException are unchecked: ArithmeticException, NullPointerException, NumberFormatException (a subclass of IllegalArgumentException), ArrayIndexOutOfBoundsException, ClassCastException. IOException and its subclass FileNotFoundException come directly from Exception: checked, the compiler wants you to catch or declare them.
What is printed?
Exception ex = new IllegalArgumentException("bad value");
System.out.println(ex.getMessage());
System.out.println(ex);
System.out.println(new RuntimeException().getMessage());Show answer
getMessage gives only the message. toString (which println calls) gives the full class name + ": " + the message. For an exception created without a message, getMessage returns null.
What is printed?
static void parse(String s) {
try {
int n = Integer.parseInt(s);
System.out.println(100 / n);
} catch (NumberFormatException | ArithmeticException e) {
System.out.println("bad input: " + e.getClass().getSimpleName());
}
}
// in main:
parse("4"); parse("x"); parse("0");Show answer
Multi-catch catches more than one type in a single block. "4": 100 / 4 = 25. "x": parseInt throws NumberFormatException. "0": parse succeeds but 100 / 0 throws ArithmeticException. The types in a multi-catch cannot be subclasses of each other; ArithmeticException | RuntimeException does not compile.
What is printed?
class InsufficientFundsException extends Exception {
private final double shortfall;
InsufficientFundsException(String msg, double shortfall) {
super(msg); this.shortfall = shortfall;
}
double getShortfall() { return shortfall; }
}
// Account.withdraw throws it when amount > balance, otherwise subtracts
Account acc = new Account(100);
try {
acc.withdraw(30);
acc.withdraw(100);
System.out.println("not printed");
} catch (InsufficientFundsException e) {
System.out.println(e.getMessage() + ", short by " + e.getShortfall());
}
System.out.println(acc.getBalance());Show answer
The first withdrawal succeeds, the balance is 70. In the second withdrawal 100 > 70: the exception is thrown, the balance does not change, and "not printed" is skipped. The shortfall is 100 − 70 = 30.0. super(msg) passes the message to Exception, and getMessage returns it.
Write a checked exception InvalidAgeException and a method int readAge(String text) that parses text (ignoring surrounding spaces) and throws InvalidAgeException with message not a number: <text> if it is not an integer, or out of range: <age> if it is outside 0 to 150.
Show answer
For a checked exception, derive from Exception. Catch the unchecked exception of parseInt and convert it to a meaningful checked exception:
public class InvalidAgeException extends Exception {
public InvalidAgeException(String msg) { super(msg); }
}public static int readAge(String text) throws InvalidAgeException {
int age;
try {
age = Integer.parseInt(text.trim());
} catch (NumberFormatException e) {
throw new InvalidAgeException("not a number: " + text);
}
if (age < 0 || age > 150)
throw new InvalidAgeException("out of range: " + age);
return age;
}"21" → 21, " 40 " → 40, "abc" → not a number: abc, "200" → out of range: 200. The caller has to write try/catch; if it does not, unreported exception InvalidAgeException.Verification: All code examples and answers were run with JDK 26 (java verify/SEN1002/W09.java; the uncaught-exception output is also from the end of the same file); the compile errors were tried with javac (verify/SEN1002/err/E09a-f.java).