Welcome back! In our previous session, we used the javap disassembler to translate a method's binary code into human-readable instructions, revealing how the JVM executes logic on its stack-based architecture. We focused on the "happy path"—the sequence of operations for normal program flow.
Today, we'll investigate what happens when things go wrong. Java's robust try-catch mechanism isn't magic; it's a well-defined process built directly into the bytecode. Our goal is to explain how the JVM implements exception handling by using a special data structure called the exception table, which is part of a method's compiled code. By the end of this lesson, you'll be able to look at the bytecode of a method and understand exactly how it will respond to a runtime error.
The Structure of a Method's Code
As we saw before, the javac compiler places the bytecode instructions for a method into the Code attribute within the .class file. However, this attribute contains more than just the instructions themselves. It also includes metadata required to execute the code, such as the maximum stack depth, the size of the local variable array, and, crucially for today's topic, an exception table.

The exception table is only present if the method contains try-catch or finally blocks. It acts as a set of rules that tells the JVM where to transfer control when an exception is thrown within a specific region of the code.
The Anatomy of an Exception Table
Each entry in an exception table consists of four parts, which define a single exception handler:
from: The starting bytecode offset (inclusive) for thetryblock.to: The ending bytecode offset (exclusive) for thetryblock.target: The bytecode offset where the handler (catchblock) code begins.type: The class of the exception that this handler can catch.
The article "How the Java virtual machine handles exceptions" from InfoWorld provides an excellent and detailed breakdown of this structure. Please read the following section, which explains the exception table using a clear example.
How the Java virtual machine handles exceptions | InfoWorld
This article offers a classic and thorough explanation of the JVM's exception-handling mechanism. The section below dissects a method's bytecode to reveal the exception table and explains the role of each of its components.
Please read the entire section starting just after the first block of bytecode. Focus on the paragraph that begins The bytecode sequence. This text explains how the from, to, target, and type fields in the table correspond to the try and catch blocks in the source code.
The Exception Handling Process
Now that you understand the structure of the exception table, let's walk through the sequence of events when an exception occurs.
-
An exception is thrown: This can happen in two ways:
- An instruction fails. For example,
irem(integer remainder) throws anArithmeticExceptionif the divisor is zero. - The code explicitly uses the
athrowinstruction, which corresponds to athrow new ...statement in Java. Theathrowinstruction pops an object reference from the operand stack and throws it as an exception.
- An instruction fails. For example,
-
The JVM searches the exception table: The JVM immediately stops normal execution. It checks the exception table of the current method, looking for an entry that satisfies two conditions:
- The current program counter (PC) is within the entry's
[from, to)range. - The type of the thrown exception is the same as, or a subclass of, the
typespecified in the entry.
- The current program counter (PC) is within the entry's
-
Control is transferred: If a matching entry is found, the JVM clears the operand stack, pushes the thrown exception object onto the now-empty stack, and sets the program counter to the entry's
targetaddress. Execution resumes inside thecatchblock. -
Stack unwinding: If no matching handler is found in the current method's exception table, the JVM does not simply crash. Instead, it unwinds the call stack. It pops the current method's stack frame and repeats the entire search process in the caller's method frame. This continues up the call stack. As you know from your work with systems programming, this process of traversing the stack is a fundamental error-handling pattern. In Java, it's a structured, automated part of the runtime. The video "JVM Anatomy 101" briefly touches upon this.
This short clip from a JetBrains presentation on JVM anatomy reinforces how the call stack structure is fundamental to enabling this efficient, cross-method exception handling.
Watch the segment from this point, where the speaker notes that the call stack allows the JVM to easily find a handler in another method and transfer control.
If the JVM unwinds all the way to the top of the thread's call stack without finding a handler, the thread terminates, and the exception's stack trace is typically printed to the console.
A Practical Example
Let's apply this to a concrete example. Consider the following Java code. Save it as ExceptionDemo.java, compile it (javac ExceptionDemo.java), and then run javap -v ExceptionDemo.class in your terminal. We use the -v (verbose) flag here because it explicitly prints the exception table, which -c does not.
// ExceptionDemo.java
public class ExceptionDemo {
public void test() {
int x = 10;
int y = 0;
try {
int z = x / y; // This will throw ArithmeticException
System.out.println(z);
} catch (ArithmeticException e) {
System.out.println("Caught a divide-by-zero error.");
}
}
}
Your javap -v output for the test method will look something like this (comments added for clarity):
public void test();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=4, args_size=1
0: bipush 10
2: istore_1 // int x = 10;
3: iconst_0
4: istore_2 // int y = 0;
5: iload_1 // try block starts here
6: iload_2
7: idiv // Throws ArithmeticException
8: istore_3
9: getstatic #2 // System.out
12: iload_3
13: invokevirtual #3 // println(int)
16: goto 24 // Skip the catch block
19: astore_3 // catch block starts here, stores exception in local 3
20: getstatic #2 // System.out
23: ldc #4 // "Caught a..."
25: invokevirtual #5 // println(String)
28: return
Exception table:
from to target type
5 17 19 Class java/lang/ArithmeticException
Look at the Exception table. It has one entry:
from 5: Thetryblock's code starts at offset 5.to 17: The block ends just before offset 17 (goto 24). The range is[5, 16].target 19: The handler code starts at offset 19.type: The handler catchesjava.lang.ArithmeticException.
When the idiv instruction at offset 7 executes, the JVM throws an ArithmeticException. It sees the PC is 7, which is in the range [5, 17). The exception type matches. The JVM jumps execution to the target, offset 19, which is the start of our catch block.
What About finally?
The finally clause introduces another layer of complexity because its code must run whether an exception is thrown or not. The compiler and JVM handle this through a clever use of the exception table and by duplicating code.
A finally block is implemented by generating:
- A
catchhandler with the specialtypeofany. This handler will catch any throwable, ensuring thefinallycode runs. After executing thefinallyblock, this handler re-throws the original exception. - Duplicating the
finallyblock's bytecode at the end of the normaltryblock execution path.
The following article gives a great walkthrough of a try-catch-finally example, showing exactly how the exception table is configured to manage this complex flow.
This article from foojay.io provides a modern, visual explanation of exception handling, with a focus on the finally block.
First, read the sections Exception table and Bytecode instructions to see how a try-catch-finally block is represented. Next, read the flow explanation to understand how the JVM navigates the different handlers. Finally, read the section Less nice flow, which clearly explains the difference between a catch and finally handler (by its type) and what happens during stack unwinding.
As you can see, the seemingly simple finally keyword results in a sophisticated bytecode arrangement, ensuring its guaranteed execution without requiring special-purpose JVM instructions.
Conclusion
In this lesson, we demystified one of Java's core runtime features. You now understand that exception handling is not an opaque, magical process but a deterministic mechanism orchestrated by the compiler and the JVM using a simple table structure.
Here are the key takeaways:
- Java's
try-catchfunctionality is implemented at the bytecode level via an exception table stored within each method'sCodeattribute. - This table maps ranges of bytecode (
tryblocks) to handler locations (catchblocks) for specific exception types. - When an exception is thrown, the JVM searches this table for a matching handler. If none is found, it unwinds the stack to search in the calling method.
- The
athrowinstruction is the bytecode equivalent of thethrowkeyword. finallyblocks are implemented through a combination of code duplication and special exception table entries that catchanythrowable type, guaranteeing their execution.
We've now spent considerable time in the weeds of method execution. In our next lesson, we will zoom out to look at the bigger picture: the core architectural components of the JVM. We'll explore the subsystems responsible for loading classes, managing memory, and executing the bytecode we've been studying.