Introduction
Exception handling in CIL is structured: the code of a method has no “try” or “catch” instruction. Instead the body ends with a table of exception clauses, each naming a range of the code (the try block) and another range (the handler) that runs when an exception is thrown inside the try block. The instructions on this page are the ones that start an exception, and the ones that are allowed to end a protected block or a handler.
The rules the specification sets: a try block is entered only at its first instruction, with an empty stack; control leaves a try block or a catch handler only with leave (or
by an exception); a finally or fault handler ends with endfinally; a filter ends with endfilter; a ret, jmp, or an ordinary
branch out of a protected block is not allowed. A handler starts with the exception object on the stack (a catch and a filter) or with an empty stack (a finally and a fault).
Exceptions and verifiability, for the whole page. These instructions are the way exceptions are raised, and a few can raise a different one (below). They are verifiable where they follow the structure rules above.
The instructions
| Opcode | Instruction | Stack | What it does | Exceptions · verifiability |
|---|---|---|---|---|
7A | throw | …, object → (empty) | Pops an object reference and throws it as an exception. The runtime looks for a matching clause, running filters, then finally and fault handlers on the way out of blocks that do not match. | Throws the object; if it is null, NullReferenceException is thrown instead. The stack is not required to be empty first. |
FE 1A | rethrow | (empty) → (empty) | Throws again the exception that the current catch handler is handling, keeping its stack trace. Valid only directly inside a catch handler (not a filter). | None of its own. |
DE i1 | leave.s target | (any) → (empty) | Leaves a try block or a catch handler: empties the evaluation stack, runs every finally handler between here and the target, and jumps to the target (a one-byte offset). | None. |
DD i4 | leave target | (any) → (empty) | The same with a four-byte offset. It is also the only way to branch out of a protected block, even when no handler has to run, so compilers write it where an ordinary branch would do. | None. |
DC | endfinally | (any) → (empty) | Ends a finally or fault handler: empties the stack and lets the runtime go on with what it was doing (the leave or the exception that started the handler). | None. Valid only at the end of a finally or fault handler. |
FE 11 | endfilter | …, int32 → (empty) | Ends a filter block. The value on top says whether to run the handler: 0 means not this one, nonzero means yes (compilers push 1). It must be the last instruction of the filter, and the handler starts right after it. | None. Valid only at the end of a filter. |
Example
The method body try { … } catch (Exception) { … } compiles to a code range for the try block ending in leave.s to the instruction after the handler, a handler range that
starts with pop (or stloc, to keep the exception) and ends in leave.s, and one clause of kind catch with the type token of Exception. The explorer shows the clause
with the IL_xxxx labels of both ranges next to the instructions.
Not described
Known, not yet covered
- Search order in detail: the two-pass model (find a handler, then run the inner finally handlers) and how filters see the stack of the throw are summarised, not described step by step.
Unknown
- How a runtime treats the clauses of a body whose ranges overlap without nesting; the explorer reports it and lists the clauses flat.
Links
Reference
- ECMA-335 – partition III: throw, rethrow, leave, endfinally, endfilter; partition I, section 12.4.2 on exception handling
- Method bodies: exception sections – how the clauses are stored