docs/runtime/exception-handling.md
Exception handling in V8 is implemented using side-tables rather than explicit bytecodes for entering and leaving try blocks. This ensures zero overhead for the "happy path" (when no exception is thrown).
The core data structure for exception handling is the HandlerTable (defined in src/codegen/handler-table.h). It maps code regions or specific instructions to their corresponding exception handlers.
There are two flavors of handler tables, depending on the execution tier:
Used by Ignition (interpreter) and Sparkplug (baseline compiler). It is stored as a TrustedByteArray attached to the BytecodeArray.
Since unoptimized code is relatively linear and compact, it is efficient to use ranges to describe try blocks.
Layout:
An array of 4-element entries:
[ range-start , range-end , handler-offset , handler-data ]
try block begins.try block ends.catch or finally block. It also encodes prediction information (whether it's likely to catch the exception) and a flag indicating if the handler was used.Zero-Overhead Try Blocks:
When generating bytecode for a try-catch statement, the BytecodeGenerator does not emit any bytecodes to "enter" the try block. Instead, it calls MarkTryBegin and MarkTryEnd on the BytecodeArrayBuilder, which records the offsets in the HandlerTableBuilder. The execution proceeds linearly.
Used by Maglev and TurboFan. It is stored directly in the instruction stream of the generated code.
Optimized code can be heavily reordered and scheduled, making ranges impractical. Instead, V8 tracks specific instructions that can throw (primarily calls) and maps their return addresses to handlers.
Layout:
An array of 2-element entries:
[ return-address-offset , handler-offset ]
Throw or ReThrow bytecode is executed, or a C++ runtime function throws an exception.BytecodeArray's HandlerTable using LookupHandlerIndexForRange.try entry) and jumps to the handler offset. If not found, it unwinds the stack frame and continues searching in the caller.Sparkplug shares the BytecodeArray with Ignition and therefore reuses the same range-based HandlerTable. When an exception occurs in Sparkplug code, it can often be handled directly in Sparkplug or it might deoptimize to Ignition to handle it.
HandlerTable using LookupReturn.In optimized code (Maglev and TurboFan), V8 sometimes uses a mechanism called Lazy Deopt on Throw for certain calls.
Instead of generating complex exception handling code (catch blocks) in the optimized code itself, V8 can mark a call descriptor with LazyDeoptOnThrow::kYes.
How it works:
kLazyDeoptOnThrowSentinel which is -1).This simplifies optimized code generation for operations where exceptions are rare or where generating optimized catch blocks would be too costly.
V8 uses "Catch Prediction" to guess whether an exception will be caught locally. This is used by the debugger to pause on uncaught exceptions.
CAUGHT: The exception is caught by a local catch block.UNCAUGHT: The exception will (likely) rethrow the exception to outside the code boundary.PROMISE: The exception will be caught and cause a promise rejection.ASYNC_AWAIT: The exception will be caught and cause a promise rejection in the desugaring of an async function.UNCAUGHT_ASYNC_AWAIT: The exception will be caught and cause a promise rejection in the desugaring of an async REPL script.[!NOTE] We cannot perform exception prediction on optimized code directly. Instead, V8 uses
FrameSummaryto find the corresponding code offset in unoptimized code to perform prediction there.