Skip to content

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.

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 again

quadruple(3) calls $double twice, producing 3 → 6 → 12.

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.

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))
WebAssembly
OptionBenefitCost
Structured call (no arbitrary jump across stack frames)The engine can validate the call graph at compile time — safer than jumping directly to an addressNo manual stack manipulation like assembly, so some patterns (e.g. classic coroutines) have to be rewritten
Recursion via plain callSimple and familiar, like recursive functions in any other languageWasm has no automatic stack growth the way some native threads do; recursion that goes too deep traps with a stack-exhaustion error
  • 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 call always adds a frame to the call stack, so deep recursion risks stack exhaustion. (The tail-call proposal reached Wasm 3.0 as explicit return_call/return_call_indirect instructions — a compiler has to emit those deliberately; ordinary call is 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.

How do you call a function named `$add` in WAT?
What must be on the stack before a `call $fn` that takes two i32 parameters?
Does basic WebAssembly optimize tail calls automatically?