Topic 7.1
What Exceptions Are
In one line
An exception is an object that describes something that went wrong. When it is thrown, Java stops the current code, walks back up the chain of method calls looking for a matching catch, and if none is found the thread dies and prints a stack trace.
Think of it like this
A fire alarm in a school. When a child in Room 12 smells smoke, they don't keep doing maths. They pull the alarm. The teacher can't fix a fire, so the message goes up to the head teacher, and if she can't handle it, to the fire brigade. Whoever can deal with it does; everyone in between stops what they were doing. An exception is that alarm: the code that finds the problem throws it, and it travels up until someone who knows what to do catches it.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Exception
- An object that describes something that went wrong while the program was running, such as dividing by zero or a missing file.
- Throw
- To signal a problem by handing an exception object to the JVM. Normal execution stops at that point.
- Catch
- To receive a thrown exception in a
catchblock and deal with it, so the program can carry on. - Call stack
- The pile of methods that are currently running, each waiting for the one above it to finish.
mainis at the bottom. - Stack frame
- One entry on the call stack: the local variables and the current position of one running method.
- Propagation (unwinding)
- An uncaught exception leaving a method and moving to its caller, frame by frame, until something catches it.
- Stack trace
- The printed list of methods (with file names and line numbers) that were active when the exception was created. It shows where the problem happened.
- `Throwable`
- The parent class of everything that can be thrown. Its two children are
ErrorandException. - Error code
- The older way to report failure: return a special value like
-1and hope every caller checks it.
Step by step
01A program without a safety net
Here main calls average, which calls divide. Integer division by zero is impossible, so the JVM creates an ArithmeticException with the message / by zero and throws it from inside divide.
Nobody catches it. divide stops, then average stops, then main stops. The JVM prints the exception and the stack trace, and the program exits with status code 1. Nothing after the failing line runs.
public class Main {
static int divide(int a, int b) {
return a / b;
}
static int average(int total, int count) {
return divide(total, count);
}
public static void main(String[] args) {
System.out.println(average(90, 0));
}
}02Reading a stack trace
Read a stack trace top down. The first line is the exception's class and message: what went wrong. The first at line is where the exception was created: divide, line 3. Each following line is the caller that was waiting: average at line 7, then main at line 11.
In real applications the trace is long, and most frames belong to frameworks. Look for the first frame that is your code; that's usually where the bug is. If the trace has a Caused by: section (Topic 7.5), the real root cause is the last Caused by:.
03Catching it: the program survives
Wrap the risky call in try { ... } catch (ArithmeticException e) { ... }. When the exception propagates out of average, the JVM finds this matching catch in main, jumps there, and gives the handler the exception object in the variable e.
After the catch block finishes, execution continues with the next statement after the whole try statement. The frames of divide and average are gone: you can't "go back" into them. Topic 7.2 covers try, catch and finally in detail.
try {
System.out.println(average(90, 0));
} catch (ArithmeticException e) {
System.out.println("could not average: " + e.getMessage()); // / by zero
}
System.out.println("still running");04An exception is just an object
new IllegalStateException("not started") creates an object exactly like new StringBuilder() does. Nothing happens until you throw it. You can store it in a variable, pass it to a method, put it in a list, or print it.
The useful methods come from Throwable: getMessage() (the text you passed, or null), toString() (class name: message), getCause(), getStackTrace() (an array of StackTraceElement), and printStackTrace() (writes the full trace to System.err).
The stack trace is captured when the object is created, not when it is thrown. Creating an exception in one method and throwing it from another shows the creation site, a detail that occasionally confuses people reading logs.
Throwable t = new IllegalStateException("not started"); // nothing thrown yet
System.out.println(t.getMessage()); // not started
System.out.println(t); // java.lang.IllegalStateException: not started
throw (IllegalStateException) t; // now it is thrown05The family tree
All throwable types form one class hierarchy, so a catch for a parent type also catches every child type. Catching Exception catches IOException and all RuntimeExceptions, but not Errors.
The split between Error, checked exceptions and unchecked (RuntimeException) exceptions is the most important map in this phase. You'll come back to it in Topic 7.3.
06Under the hood: the exception table
A try block adds no instructions to the happy path. Instead, javac writes an exception table next to the method's bytecode. Each row says: if an exception of this type happens between instruction from and instruction to, jump to target.
When something is thrown, the JVM checks the current method's table for a row covering the current instruction with a matching type. If one matches, it clears the operand stack, pushes the exception, and jumps. If not, it pops the frame and repeats in the caller. That's stack unwinding, done by the JVM, not by your code.
static int safeDivide(int a, int b) {
try {
return a / b;
} catch (ArithmeticException e) {
return 0;
}
}07Exceptions vs error codes
With error codes, int r = parse(text); if (r == -1) ... the check is optional, and -1 might also be a valid answer. Every caller in the chain must check and pass the code upward by hand.
With exceptions, the code that detects the problem throws, the code that can handle it catches, and everything in between stays clean. The price: control can leave any method at any point where something can be thrown, so you must write code that stays correct when interrupted. That's what finally and try-with-resources (Topics 7.2 and 7.6) are for.
Try it yourself
- 1
Predict the skipped lines
In the first example, change
average(90, 3)toaverage(90, 0)too (outside thetry). Before running, write down every line you expect to see and what happens tomain carries on. Then run it. The program now ends with a stack trace instead of the last line. - 2
Add a level to the stack
In the stack-trace example, add
level0()that callslevel1(), and calllevel0()frommain. Predict the new list of frames, then run. - 3
Find the parent classes
In the hierarchy example, add
new NumberFormatException("x")andnew java.io.FileNotFoundException("x")to the array. Predict the three true/false columns for each, then run. Then printgetSuperclass()for both.
Code & diagrams
With a zero count, the 'finished' lines never print: each frame is abandoned as the exception passes through it.
Expected output
main starts
average starts
divide(90, 3) starts
divide finished
average finished
result: 30
average starts
divide(90, 0) starts
caught in main: java.lang.ArithmeticException: / by zero
main carries onExpected output
ArithmeticException | message=/ by zero | Exception? true | Runtime? true | Error? false
IOException | message=disk not ready | Exception? true | Runtime? false | Error? false
StackOverflowError | message=null | Exception? false | Runtime? false | Error? true
IllegalArgumentException | message=age must be >= 0 | Exception? true | Runtime? true | Error? false
java.lang.IllegalStateException: not started
parent class: java.lang.RuntimeExceptionLine numbers are left out on purpose so the output is stable. getLineNumber() gives them when you need them.
Expected output
message: deep problem
at Main.level3
at Main.level2
at Main.level1
at Main.main// Error-code style: easy to forget the check, and -1 might be a real value
static int parseAgeOrMinusOne(String text) {
for (char c : text.toCharArray()) {
if (!Character.isDigit(c)) return -1;
}
return Integer.parseInt(text);
}
// Exception style: the failure can't be silently ignored
static int parseAge(String text) {
int age = Integer.parseInt(text); // throws NumberFormatException for "abc"
if (age > 150) {
throw new IllegalArgumentException("age too large: " + age);
}
return age;
}Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Index past the end of an array
In main, write int[] marks = {90, 80}; System.out.println(marks[2]); with no try/catch.
Break #2
Recurse forever
Write static int countDown(int n) { return countDown(n - 1); } (no base case) and call it from main.
Myth vs fact
Myth
An exception means the program crashes.
Fact
Only an uncaught exception ends the thread. Caught exceptions are a normal, controlled way to handle failure, and well-written servers catch and handle them all day without stopping.
Myth
try blocks slow the code down.
Fact
Entering a try block costs nothing: javac only records an exception table. The cost is in creating the exception (capturing the stack trace) and unwinding, which only happens when something is actually thrown.
Myth
Errors and exceptions are the same thing.
Fact
Error and Exception are sibling classes under Throwable. Errors like OutOfMemoryError mean the JVM itself is in trouble; catch (Exception e) does not catch them, and application code should rarely try.
Myth
The stack trace shows where the exception was thrown.
Fact
It shows where the exception object was created (fillInStackTrace runs in the constructor). Usually that's the same line, but not when an exception is created in one place and thrown elsewhere.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
The JVM can throw some exceptions without allocating a fresh object or trace. With
-XX:+OmitStackTraceInFastThrow(on by default in HotSpot), a JIT-compiled hot path that throws the same implicitNullPointerException,ArithmeticExceptionorArrayIndexOutOfBoundsExceptionmany times may switch to a preallocated exception with no message and no stack trace. If your logs suddenly showjava.lang.NullPointerExceptionwith no trace, look further back for the first occurrence, or run with-XX:-OmitStackTraceInFastThrow. - ▸
Throwablehas a protected constructorThrowable(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace)(Java 7). PassingwritableStackTrace = falseskipsfillInStackTrace()and makes creation cheap. Some high-performance libraries use stackless exceptions for control flow. OverridingfillInStackTrace()to returnthiswas the older trick. - ▸
Stack unwinding is driven by the exception table: each row's range is half-open (
frominclusive,toexclusive), rows are searched in order, and thetypeis matched withinstanceofsemantics. Afinallyblock appears as extra rows with typeany(Topic 7.2). - ▸
StackWalker(Java 9) lets you inspect the current stack lazily without creating an exception, which is the right tool for logging the caller's name.Thread.setDefaultUncaughtExceptionHandlerreplaces the JVM's default 'print and die' behaviour for threads that don't catch.
Remember this
- 1
An exception is an ordinary Java object. It's an instance of a class that extends
java.lang.Throwable, created withnewon the heap like any other object. It carries a message (getMessage()), an optional cause (another exception that led to this one), and a stack trace: the list of method calls that were active when it was created. - 2
Throwing an exception (
throw new IllegalArgumentException("age < 0"), or the JVM doing it for you on10 / 0) stops normal execution immediately. The rest of the current statement and method is skipped. The JVM looks for acatchblock that matches in the current method; if there isn't one, the method's stack frame is thrown away and the search continues in the caller. This walk up the call stack is called propagation or stack unwinding. - 3
If the exception reaches the top of a thread's stack without being caught, the thread ends. For the
mainthread, the JVM's default handler printsException in thread "main", the exception's class and message, and the stack trace, then the program exits with status 1. Other threads keep running if they're not daemon threads (that's a concurrency subject for later). - 4
The hierarchy matters.
Throwablehas two branches. **Error** is for serious problems in the JVM itself, such asOutOfMemoryErrorandStackOverflowError; ordinary code shouldn't try to recover from them. **Exception** is for problems an application might handle. UnderException, **RuntimeException** and its subclasses (NullPointerException,IllegalArgumentException,ArithmeticException,IndexOutOfBoundsException...) are unchecked; everything else underException(IOException,SQLException...) is checked, which Topic 7.3 explains. - 5
Exceptions replace error codes. In C, a function returns
-1and every caller must remember to check it. Forget once and the program carries on with garbage. A Java exception can't be silently ignored by accident: either some code catches it, or the thread stops with a clear report of what failed and where. It also separates the happy path from the error handling, so the main logic reads top to bottom. - 6
Exceptions aren't free. Creating one calls
fillInStackTrace(), which walks the current stack and records every frame, and that's the expensive part (throwing and catching within one method is comparatively cheap). That's why exceptions are for exceptional situations, not for normal control flow like ending a loop. A try block itself costs nothing at run time when no exception is thrown: the compiler records the protected range in an exception table instead of adding instructions.
Explain it without notes
What is an exception in Java, and what happens step by step when one is thrown?
What is the difference between Error, checked exceptions and unchecked exceptions?
How do you read a stack trace to find the bug?
Why are exceptions better than returning error codes, and what do they cost?
Practice
Write a program that calls Integer.parseInt("12a") inside a try block and prints the exception's simple class name and message.
Write three methods a(), b(), c() where c() throws new UnsupportedOperationException("not yet"). Catch it in main and print how many frames the stack trace has and the name of the top method.
Build an array of three Throwables (new OutOfMemoryError(), new java.io.IOException(), new NullPointerException()) and, for each, print whether a catch (Exception e) block would catch it.
Trade-offs
- ↔
Exceptions vs return values: exceptions are right for failures the immediate caller usually can't handle; for an expected 'not found' outcome, returning
Optionalor a result type (Phase 10) is cheaper and makes the outcome part of the signature. - ↔
Fail fast vs carry on: letting a bug's exception propagate stops the work but gives a clear trace; catching it too early and continuing hides the bug and corrupts data later. Catch only where you can actually do something useful.
- ↔
Rich traces vs speed: stack traces are what make exceptions debuggable, but capturing them costs time. Keep them for real failures, and don't throw exceptions in tight loops as a substitute for an
if.
Done when you can
Done when you can explain what happens on the call stack when an exception is thrown and not caught.
Done when you can read a stack trace and point to the line that failed and the chain of callers.
Done when you can draw the
Throwable/Error/Exception/RuntimeExceptionhierarchy from memory.Done when you can use
getMessage(),toString()andgetStackTrace()on a caught exception.Done when you can say why a
tryblock is free but throwing an exception is not.