💥 What Happens When an Exception is Thrown?
When your code executes something like:
throw new IllegalArgumentException("Bad argument");
The JVM does several things:
1️⃣ Creating the Exception Object
- A new instance of
IllegalArgumentExceptionis created usingnew. - The constructor initializes the stack trace (this is why you get the full trace when printing an exception).
2️⃣ Unwinding the Stack (a.k.a. Stack Unwinding)
- The JVM immediately looks for a
catchblock in the current method (the method that threw the exception). - If no appropriate
catchis found in the method, the method exits (pops off the stack), and the exception propagates to the caller method. - This repeats until either:
- A
catchblock is found that handles this exception type, or - The main method exits and the exception reaches the JVM itself.
- A
3️⃣ Exception Table Lookup
Each method’s bytecode includes an exception table. This table maps:
- Code ranges (start/stop of a
tryblock) - The exception types each
catchblock can handle - The bytecode offset for each corresponding
catchblock
Example (simplified table):
| Start | End | Handler PC | Exception Type |
|---|---|---|---|
| 10 | 30 | 100 | java/lang/NullPointerException |
| 10 | 30 | 120 | java/lang/Exception |
The JVM uses this table to decide where control flow should jump when an exception is thrown.
4️⃣ Propagation
If no handler exists in the current method, the method:
- Cleans up local variables (local variable table is discarded)
- Releases locks held by
synchronizedblocks (this is crucial for avoiding deadlocks) - Pops off the stack
- Passes the exception to the caller method (next stack frame)
This continues up the call stack until:
- A handler (
catch) is found, or - The stack is fully unwound — if so, the exception is printed to
stderr(what you see as the “stack trace”) and the program exits with a non-zero status.
5️⃣ Printing Stack Trace
- When you call
printStackTrace(), the JVM actually uses the stack trace captured when the exception was created (part ofThrowable). - This is why
new Exception()has a performance cost — it captures the whole stack at the point of creation.
📋 Quick Diagram — JVM Exception Flow
+----------------+
| Method X calls Y |
+----------------+
|
v
+-----------------+
| Method Y throws |
| NullPointerException |
+-----------------+
|
v
+---------------------+
| JVM checks Y's exception table |
+---------------------+
|
v
+------------------------------------+
| No catch found — unwind stack frame |
+------------------------------------+
|
v
+--------------------+
| Control returns to X |
+--------------------+
|
v
+---------------------------+
| X has a catch (maybe) or exits |
+---------------------------+
|
v
+--------------------+
| Stack fully unwinds |
+--------------------+
|
v
+----------------------------------+
| JVM prints stack trace and exits |
+----------------------------------+
⚠️ Special Case: finally
finallyblocks always run (even if an exception occurs).- When exceptions are thrown, the JVM inserts synthetic bytecode to ensure
finallyruns before the stack unwinds. - You can see this in bytecode dumps —
finallyblocks often appear as duplicated blocks of code at the end oftryblocks.
🛠️ Tools to See It in Action
If you want to observe this:
- Use
javap -c YourClassto see the bytecode (including exception tables). - Run with
-XX:+TraceExceptions(depending on the JVM, this can show internal exception tracing).
🚀 In Short
| Step | Action |
|---|---|
| 1 | Exception object created |
| 2 | Stack unwinds, finding handlers using the exception table |
| 3 | finally blocks always run |
| 4 | If no handler found, exception reaches top-level and JVM exits |
| 5 | Stack trace printed (captured at creation) |