Calls and Recursion
Functions in WebAssembly call each other with the call instruction. Arguments are pushed onto the stack before the call; the return value (if any) is left on the stack when the callee returns. Wasm fully supports direct recursion — a function may call itself — and mutual recursion between functions defined in the same module.
The call instruction
Section titled “The call instruction”call $funcname invokes the named function. You can also call by index (call 0) but named references are far easier to read. Before executing call, push all arguments in left-to-right order:
(module (func $double (param $n i32) (result i32) local.get $n i32.const 2 i32.mul) (func (export "quadruple") (param $x i32) (result i32) local.get $x call $double ;; passes $x; result replaces it on stack call $double)) ;; doubles againquadruple(3) calls $double twice, producing 3 → 6 → 12.
Recursive functions
Section titled “Recursive functions”WAT allows a function to call itself. The pattern mirrors every other language: push arguments, call self, combine results. The base case uses if to return directly without a recursive call.
(module (func $factorial (export "factorial") (param $n i32) (result i32) local.get $n i32.const 1 i32.le_s ;; n <= 1? if (result i32) i32.const 1 ;; base case: 0! = 1! = 1 else local.get $n local.get $n i32.const 1 i32.sub call $factorial ;; factorial(n-1) i32.mul ;; n * factorial(n-1) end))The function is given both an internal name ($factorial) so it can call itself and an export name so JavaScript can invoke it.
Fibonacci example
Section titled “Fibonacci example”Two recursive calls in the else arm work the same way — push arguments, call, then add the two results:
(module (func $fib (export "fib") (param $n i32) (result i32) local.get $n i32.const 1 i32.le_s ;; n <= 1? if (result i32) local.get $n ;; fib(0)=0, fib(1)=1 else local.get $n i32.const 1 i32.sub call $fib ;; fib(n-1) local.get $n i32.const 2 i32.sub call $fib ;; fib(n-2) i32.add end))Run it
Section titled “Run it”Trade-offs
Section titled “Trade-offs”| Option | Benefit | Cost |
|---|---|---|
Structured call (no arbitrary jump across stack frames) | The engine can validate the call graph at compile time — safer than jumping directly to an address | No manual stack manipulation like assembly, so some patterns (e.g. classic coroutines) have to be rewritten |
Recursion via plain call | Simple and familiar, like recursive functions in any other language | Wasm has no automatic stack growth the way some native threads do; recursion that goes too deep traps with a stack-exhaustion error |
Common mistakes
Section titled “Common mistakes”- Writing a recursive function without a complete base case (e.g. forgetting to guard
n < 0), so the recursion never terminates - Expecting Wasm to tail-call-optimize automatically like some languages do — every
callalways adds a frame to the call stack, so deep recursion risks stack exhaustion. (The tail-call proposal reached Wasm 3.0 as explicitreturn_call/return_call_indirectinstructions — a compiler has to emit those deliberately; ordinarycallis never silently turned into one.) - Pushing arguments in the wrong order before
call(WAT has no named arguments), so a parameter silently binds to the wrong position with no compile-time error
💡 Real-world example
The WebAssembly spec requires every conforming engine to enforce a call-stack limit: unbounded recursion traps with a well-defined runtime error instead of crashing the host process the way an unguarded native stack overflow can. This spec-mandated guarantee is part of what makes it safe to run untrusted Wasm code at all.