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.
The if construct
Section titled “The if construct”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.
if without a result
Section titled “if without a result”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: branchless ternary
Section titled “select: branchless ternary”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 falseRun it
Section titled “Run it”Trade-offs
Section titled “Trade-offs”| Option | Benefit | Cost |
|---|---|---|
Structured if/else expression | Validated at compile time — both arms must leave the same type and count, safer than an arbitrary goto | Very 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 predict | Both values must always be computed up front, even the arm that isn’t used — wasteful if one side is expensive |
Common mistakes
Section titled “Common mistakes”- Forgetting that the
thenandelsearms must leave the same type and count declared by(result i32), so the module fails to validate - Using
selectand expecting short-circuit evaluation like a JavaScript ternary — the engine actually evaluates (or at least pushes) both values before choosing one - Confusing
br_ifwithif—br_ifhas noelsearm of its own; it only conditionally branches out of a single block, it isn’t a two-way conditional likeif/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/elsestructure that matches the original code’s arbitrary control-flow graph.