Skip to content

Conditionals: if, else, and select

WebAssembly’s if construct is both a control-flow branch and an expression. It pops an i32 condition from the stack, executes one of two arms, and — if given a result type — leaves a value on the stack. This makes if feel more like a ternary operator than a traditional if statement.

if pops the top i32. A non-zero value executes the then arm; zero executes the optional else arm. The construct ends with end.

(module
(func (export "sign") (param $x i32) (result i32)
local.get $x
i32.const 0
i32.gt_s ;; x > 0?
if (result i32)
i32.const 1 ;; then: return 1
else
i32.const -1 ;; else: return -1
end))

The (result i32) annotation declares that the if expression leaves one i32 on the stack regardless of which arm executes. Both arms must leave the same number and types of values.

When the condition is purely for side effects — for example calling an imported function — you can omit the result type:

(module
(func (export "clampNeg") (param $x i32) (result i32)
local.get $x
i32.const 0
i32.lt_s
if
i32.const 0
local.set $x ;; clamp $x to 0 if negative
end
local.get $x))

Here the if has no result; it just conditionally updates a local.

select is a compact alternative to if for simple value choices. It pops three values: condition, val_if_false, val_if_true (in push order: push true-val, push false-val, push condition). It pushes the chosen value. No branch is taken — the CPU evaluates both values before the select.

(module
(func (export "minVal") (param $a i32) (param $b i32) (result i32)
local.get $a ;; val if condition is true ($a <= $b)
local.get $b ;; val if condition is false
local.get $a
local.get $b
i32.le_s ;; condition: a <= b?
select)) ;; choose $a if true, $b if false
WebAssembly
OptionBenefitCost
Structured if/else expressionValidated at compile time — both arms must leave the same type and count, safer than an arbitrary gotoVery complex conditionals (many crossing branches) need nested ifs or helper blocks instead of jumping straight to a point
select (branchless)No branch at all — good for hot paths that are hard to predictBoth values must always be computed up front, even the arm that isn’t used — wasteful if one side is expensive
  • Forgetting that the then and else arms must leave the same type and count declared by (result i32), so the module fails to validate
  • Using select and expecting short-circuit evaluation like a JavaScript ternary — the engine actually evaluates (or at least pushes) both values before choosing one
  • Confusing br_if with if — br_if has no else arm of its own; it only conditionally branches out of a single block, it isn’t a two-way conditional like if/else

💡 Real-world example

When Emscripten compiles C/C++ containing a ternary or a complex if-else chain, its relooper algorithm reconstructs a validatable if/else structure that matches the original code’s arbitrary control-flow graph.

What does `if` pop to decide which branch to take?
What is `select` in WAT?
What keyword closes an `if` block in WAT?