Skip to content

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.

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.

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 in a block exits forward; br in a loop repeats backward

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 x

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)
WebAssembly
OptionBenefitCost
Structured control flow (block/loop/br)Statically validatable and safer — there is no arbitrary goto that can jump anywhereComplex control flow is harder to write; you nest several blocks instead of jumping straight to a point
No raw assembly-style jumpsThe engine can check every label at compile time, reducing security riskA 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
  • Miscounting nesting depth when several blocks are nested, so br or br_table targets the wrong label (an off-by-one in depth)
  • Forgetting that br_table always needs a default label — any index past the end of the list falls through to the default automatically
  • Mixing up the direction of br when switching from block to loop — 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/br shape Wasm requires, because native code is full of gotos that Wasm doesn’t support directly.

Where does `br $b` jump when `$b` is a `block` label?
What does `br_if $l` pop from the stack?
Which instruction implements a jump table in WAT?