throw new TimeoutException() looks like a jump, but it's a search: the runtime must find the nearest matching catch by walking frames that were never designed for this. The mechanism — zero-cost exception handling — is a masterpiece of deferred cost: free on the happy path, expensive exactly when used.
The throw evaluates the exception object (one heap allocation — the malloc-trace fast path) and transfers control to the runtime's throw machinery. Critically, the current function does not return normally: no epilogue runs, no locals are destructed by normal flow — the unwind must clean up (C++ destructors, Java finally blocks, JS deferred blocks) as it goes. The compiler has pre-built a map of every cleanup point; that map is the machinery.
The runtime — actually the personality routine in the exception tables (.gcc_except_table in ELF, __cxa_throw in Itanium ABI, JS's built-in walk) — uses the current instruction pointer to look up the frame's action table: which catch types this frame can handle and which cleanup blocks must run. This is a binary search over a sorted table of IP ranges, ~100 ns, entirely data-driven. There is no per-frame "try" flag in registers or on the stack — that's the zero-cost trick: the happy path pays nothing.
If the current frame can't handle the exception, the runtime unwinds: restore the frame's registers, jump to its cleanup/landing pad if one exists (which runs destructors, then rethrows implicitly), then pop to the caller and repeat the table lookup at the return address. Each frame is a lookup plus a register restore — ~1–2 µs per frame in the common case, more when cleanup code actually runs. Deep stacks make throws expensive: 50 frames × 1.5 µs = 75 µs just to find the handler.
The search lands on a frame whose action table lists a matching catch type; the runtime materializes the handler's frame state (registers, stack pointer) from the table, binds the exception object, and jumps into the catch block. From here it's ordinary code — the exception is just a parameter. The try/catch boundary has cost nothing before, and costs a bounded constant now; the catch itself runs at normal speed.
The cost model is brutally asymmetric. A function with a try block costs zero extra cycles when nothing throws — no setjmp bookkeeping, no checks. A throw costs ~µs per frame unwound plus handler materialization — 1,000–10,000× a normal return. That asymmetry is why "exceptions are slow" is wrong and right at once: they're free until used, and catastrophic when used in a loop. The Java -XX:+PrintCompilation folklore — the JIT recompiling try methods because the throw path defeats C2's inlining — is the visible symptom.
# Java microbenchmark: throw/catch in a loop vs a return-code in a loop
# Expect 2-3 orders of magnitude: ~20ns for the flag path, ~5-20us per throwWhat the machine actually does is consult a compiler-built table of handlers, walk frames by restoring registers and running cleanups until one table entry matches, then materialize the handler and continue as if nothing happened. The happy path truly costs nothing — and the throw path costs exactly what it deserves: the search you asked for.