💥 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
IllegalArgumentException
is 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
catch
block in the current method (the method that threw the exception). - If no appropriate
catch
is found in the method, the method exits (pops off the stack), and the exception propagates to the caller method. - This repeats until either:
- A
catch
block 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
try
block) - The exception types each
catch
block can handle - The bytecode offset for each corresponding
catch
block
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
synchronized
blocks (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
finally
blocks always run (even if an exception occurs).- When exceptions are thrown, the JVM inserts synthetic bytecode to ensure
finally
runs before the stack unwinds. - You can see this in bytecode dumps —
finally
blocks often appear as duplicated blocks of code at the end oftry
blocks.
🛠️ Tools to See It in Action
If you want to observe this:
- Use
javap -c YourClass
to 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) |