Branches: block, br, br_if, br_table
Branching in WebAssembly is always relative to a labeled construct — never to a raw memory address. The br family of instructions targets the nesting level, not a line number. This makes the behaviour of br depend entirely on what kind of construct it targets.
block and br
Section titled “block and br”A block is a forward-only group of instructions. When br $label targets a block, execution jumps to the instruction immediately after the block’s closing end. Think of it as a labeled break.
(module (func (export "earlyExit") (result i32) (block $b (result i32) i32.const 42 br $b ;; exits the block; 42 is the block result i32.const 99) ;; never reached ))The (result i32) annotation on the block declares that the block leaves one i32 on the stack when it exits — either by falling through to end or by branching out with br.
br targets direction
Section titled “br targets direction”The same br instruction behaves differently depending on what it targets:
flowchart LR A["block $b\n ...body...\n br $b"] --> B["jumps to END\nof block (forward)"] C["loop $l\n ...body...\n br $l"] --> D["jumps to START\nof loop (backward)"]
br_if: conditional branch
Section titled “br_if: conditional branch”br_if $label pops an i32 condition from the stack. If it is non-zero the branch fires; otherwise execution continues with the next instruction. This is the idiomatic way to write a conditional early exit.
(module (func (export "relu") (param $x i32) (result i32) (block $done (result i32) i32.const 0 ;; block result if we exit early local.get $x i32.const 0 i32.lt_s ;; x < 0? br_if $done ;; if yes, exit block — 0 is already on stack drop ;; discard the 0; x >= 0 local.get $x))) ;; return xbr_table: jump table
Section titled “br_table: jump table”br_table takes a list of labels and a default. It pops an i32 index and jumps to the label at that index, or to the default if the index is out of range. It is the WAT equivalent of a switch statement.
(module (func (export "describe") (param $n i32) (result i32) (block $two (block $one (block $zero local.get $n br_table $zero $one $two $two) ;; index 0→$zero, 1→$one, ≥2→$two i32.const 0 return) ;; $zero case: return 0 i32.const 1 return) ;; $one case: return 1 i32.const 2)) ;; $two case: return 2 (falls through as the function result)Run it
Section titled “Run it”Trade-offs
Section titled “Trade-offs”| Option | Benefit | Cost |
|---|---|---|
Structured control flow (block/loop/br) | Statically validatable and safer — there is no arbitrary goto that can jump anywhere | Complex control flow is harder to write; you nest several blocks instead of jumping straight to a point |
| No raw assembly-style jumps | The engine can check every label at compile time, reducing security risk | A control-flow graph coming from native code (full of goto) has to go through an algorithm like Emscripten’s relooper before it can map onto Wasm |
Common mistakes
Section titled “Common mistakes”- Miscounting nesting depth when several
blocks are nested, sobrorbr_tabletargets the wrong label (an off-by-one in depth) - Forgetting that
br_tablealways needs a default label — any index past the end of the list falls through to the default automatically - Mixing up the direction of
brwhen switching fromblocktoloop— the same instruction goes forward in a block but backward in a loop; get the label wrong and the flow goes the wrong way
💡 Real-world example
Emscripten’s relooper algorithm — designed by Emscripten’s author for exactly this problem — converts an arbitrary control-flow graph from C/C++ source into the structured
block/loop/brshape Wasm requires, because native code is full ofgotos that Wasm doesn’t support directly.