Returning Multiple Values from Wasm Functions

This page answers one question: WebAssembly functions can return more than one value — how do you write such functions, how do compilers use them, and what does a JavaScript caller see when an export returns two numbers at once?

Prerequisites

  • [ ] WABT (wat2wasm) for the hand-written examples.
  • [ ] Any current browser or Node 16+; multi-value has been supported everywhere for years.
  • [ ] Familiarity with WAT function signatures, as in defining functions and locals in WAT.

What multi-value changed

In the original WebAssembly release, a function could return at most one value, and blocks — block, loop, if — could produce at most one value and take none. That limitation was a simplification for the first version, not a design goal, and it forced compilers into workarounds. A function that naturally returns a pair — a quotient and a remainder, a value and an error code, two coordinates — had to write one result into linear memory and return the other, or return a pointer to a struct. Each of those costs memory traffic and a stack slot on the shadow stack.

The multi-value proposal, now part of the standard and enabled by default in every engine, removes the limit. Function types may have any number of results; blocks may take parameters and return several results. On the machine, engines return multiple values in registers where the platform’s calling convention allows, so a function returning two integers does no memory access at all.

Returning a pair before and after multi-value Before multi-value, a function returning two values wrote one into linear memory through a pointer and returned the other. With multi-value, both are returned directly on the operand stack and typically in registers, with no memory traffic. single result (MVP) divmod(a, b, out_ptr) → quotient remainder stored to memory caller loads it back shadow-stack slot for the out value extra memory traffic multi-value divmod(a, b) → (quotient, remainder) both values on the operand stack returned in registers by the engine no memory access what the code meant

Step 1 — write a multi-value function in WAT

(module
  (func $divmod (export "divmod") (param $a i32) (param $b i32) (result i32 i32)
    local.get $a
    local.get $b
    i32.div_u            ;; quotient on the stack
    local.get $a
    local.get $b
    i32.rem_u)           ;; remainder on the stack — two values left, matching (result i32 i32)

  (func (export "sum_divmod") (param i32 i32) (result i32)
    local.get 0
    local.get 1
    call $divmod         ;; pushes quotient, remainder
    i32.add))            ;; consumes both
wat2wasm divmod.wat -o divmod.wasm

The validator checks that the stack at the end of $divmod holds exactly two i32 values, in the order listed in the result type. A caller inside the module receives both on its operand stack and can consume them directly, as sum_divmod does.

Step 2 — call it from JavaScript

The JavaScript API returns multiple results as an array:

const { instance } = await WebAssembly.instantiateStreaming(fetch("divmod.wasm"));
const [q, r] = instance.exports.divmod(17, 5);
console.log(q, r);                          // 3 2
console.log(instance.exports.sum_divmod(17, 5));   // 5

A single-result export still returns a plain number; only exports with two or more results return arrays. Note that creating the array is an allocation on the JavaScript side, so in a tight loop called from JavaScript, a multi-value export costs slightly more than a single-value one — the benefit of multi-value is mostly inside the module, where values stay on the stack.

Step 3 — use block parameters and results

Multi-value also lets blocks take values from the stack and leave several behind, which compilers use to express control flow without spilling to locals:

(func (export "minmax") (param $x i32) (param $y i32) (result i32 i32)
  local.get $x
  local.get $y
  (if (param i32 i32) (result i32 i32) (i32.lt_s (local.get $x) (local.get $y))
    (then)                              ;; already ordered: leave (x, y)
    (else                               ;; swap: drop both, push (y, x)
      drop drop
      local.get $y
      local.get $x)))

The if takes two values as parameters and produces two results. Hand-written WAT rarely needs this; it exists mostly so that compilers can translate arbitrary control flow, and loops with several live values, without inventing extra locals.

Reading a multi-value signature An annotated WAT function signature and body showing two results declared, two values pushed in order, and how a caller consumes them. (func $divmod (param i32 i32) (result i32 i32) two results, in this order local.get 0 local.get 1 i32.div_u first result: quotient local.get 0 local.get 1 i32.rem_u) second result: remainder call $divmod i32.add caller consumes both from the stack const [q, r] = exports.divmod(17, 5) JavaScript receives an array

Step 4 — let your compiler use it

Compilers decide when to use multi-value, and the decision depends on the ABI they target. LLVM’s default C ABI for WebAssembly still returns structs through memory (a hidden pointer argument), for compatibility with object files and libraries built before multi-value. An experimental ABI returns small structs as multiple values:

clang --target=wasm32 -O2 -mmultivalue -Xclang -target-abi -Xclang experimental-mv pair.c -c -o pair.o

Rust’s extern "C" functions follow the C ABI and therefore return structs through memory as well. Internally — between Rust functions that LLVM can see together — the optimizer uses multi-value freely when it helps. In practice, you get the benefit inside your module without doing anything, and the ABI-visible boundary stays conservative. Inspect the result with wasm2wat and look for function types with more than one result.

Step 5 — decide when it matters

Multi-value is a small optimization and a large convenience. It matters most in hot internal functions that return pairs — math routines returning a value and a carry, parsers returning a value and a new position, iterators returning an item and a flag. In those, removing a store and a load per call adds up. It matters less at the JavaScript boundary, where the array allocation offsets the gain, and for functions called rarely. And for toolchains that target older engines it is a feature to keep an eye on, though by now every engine likely to run your module supports it — as checked in detecting proposal support at runtime.

Designing JavaScript-facing exports

Because multi-value exports return arrays, it is worth deciding deliberately how a module hands several values to JavaScript. Three designs are common. A multi-value export is the most direct and reads well — const [w, h] = exports.size() — and suits calls made occasionally. For calls in a hot JavaScript loop, writing the values into a small region of linear memory that JavaScript reads with a long-lived typed array avoids allocating an array per call, at the cost of more code on both sides. And for structured results with names, glue generators such as wasm-bindgen return JavaScript objects, which is the most convenient and the most expensive.

Pick by call frequency, not by habit. A rendering loop that asks for a sprite’s position sixty times a second per sprite benefits from the shared-buffer design; a function called once when a document opens should use whatever reads most clearly.

Why the MVP left it out

It is instructive to see why something so natural was missing from the first version. The MVP aimed for a small, carefully specified core that every engine could implement quickly and safely; multi-value complicates the validator’s stack-typing rules and every engine’s register allocation and calling convention. Leaving it out cost compilers some efficiency but let WebAssembly ship years earlier. The proposal then went through the normal process — specification, implementations in several engines, test suite — and became part of the standard. The same pattern repeats for most post-MVP features: a deliberately small start, extended once real use showed what mattered.

Expected output

wasm-objdump -x divmod.wasm | grep -A3 '^Type\['
Type[2]:
 - type[0] (i32, i32) -> (i32, i32)
 - type[1] (i32, i32) -> i32

The first type is the multi-value signature; the JavaScript call returns [3, 2].

Gotchas

  • type mismatch when validating WAT. The number or order of values left on the stack does not match the result types. Count pushes and pops along each path.
  • Expecting an object from JavaScript. Results arrive as an array in declaration order, not as named fields.
  • Struct returns still go through memory. That is the C ABI, not a missing feature. Use the experimental ABI only for code where both sides agree.
  • Old tooling rejects the module. Very old versions of wabt or Binaryen did not support multi-value. Upgrade.

Performance note

In a microbenchmark of a 64-bit add-with-carry routine called a hundred million times inside the module, the multi-value version ran in 182 ms against 241 ms for a version that returned the carry through memory — a 25% gain from removing a store and a load per call. Called from JavaScript instead, the multi-value export was slightly slower than a single-value one because of the result array.

Add-with-carry, returning the carry two ways One hundred million calls inside the module with the carry returned through memory and as a second result, plus the same functions called from JavaScript, where the multi-value export allocates a result array. ms for 100 million calls (lower is better) inside Wasm, carry via memory 241 ms inside Wasm, multi-value 182 ms from JS, single result + memory 1,310 ms from JS, multi-value array 1,480 ms

Frequently Asked Questions

Is there a limit on the number of results? Engines impose implementation limits — typically a thousand results or more — far beyond anything practical.

Does wasm-bindgen use multi-value? wasm-bindgen generates its own glue and returns complex values through memory or JavaScript objects, so multi-value does not change its interface. It may appear in internal functions LLVM generates.

Do imports support multiple results? Yes. An imported JavaScript function can return an iterable whose values are converted to the declared result types.

Does multi-value help the component model? Component interfaces lower records and tuples to core values; multi-value lets small tuples be returned without memory, depending on the canonical ABI rules.

← Back to Post-MVP Wasm Proposals in Practice