Skip to content

WasmFX (WebAssembly Stack Switching) ​

A typed, low-level continuation primitive for WebAssembly: a single stack-switching mechanism that compilers target to implement async/await, generators, coroutines, lightweight threads, and effect handlers, instead of relying on whole-program CPS or state-machine transforms.

Last reviewed: June 2, 2026.

FieldValue
EcosystemWebAssembly Community Group / Working Group proposal
Proposal repoWebAssembly/stack-switching
Authoritative specStack-switching Explainer.md (current instruction set)
ChampionsFrancis McCabe, Sam Lindley
StandardizationPhase 3 — Implementation Phase (CG + WG), per WebAssembly proposals tracker
Research originContinuing WebAssembly with Effect Handlers (OOPSLA 2023) — the WasmFX paper
Project siteWasmFX project site (the original "typed continuations" explainer)
Reference interp.OCaml interpreter under interpreter/ in WebAssembly/stack-switching
Base specWasm 3.0 + function-references + exception-handling

Overview ​

What It Solves ​

Industrial languages rely on non-local control flow — async/await, coroutines, generators/iterators, effect handlers, lightweight (green) threads — and for some languages (Go, Erlang, OCaml 5) it is central to identity and to scalable concurrency. Compiling these features to a Wasm without first-class stacks forces toolchains into whole-program transforms (CPS, Asyncify-style state machines), which bloat code, destroy the natural call-stack structure (hurting debuggers and stack traces), and compose poorly across module boundaries.

The Explainer states the design strategy directly: "Rather than build specific control flow mechanisms for all possible varieties of non-local control flow, our strategy is to build a single mechanism, continuations, that can be used by language providers to construct their own language specific features" (proposals/stack-switching/Explainer.md, Motivation). WasmFX is the research line — "Continuing WebAssembly with Effect Handlers", OOPSLA 2023 — that produced this design; the standards-track spec carries it forward under the name stack switching.

Design Philosophy ​

The proposal is a low-level substrate, not a high-level effect API. It reuses Wasm's existing machinery wherever possible:

  • It reuses tags from the exception-handling proposal, generalizing them so a tag may also carry result types — "a control tag may be thought of as a resumable exception" (Explainer, Declaring control tags).
  • It adds exactly one new reference type (cont) and seven new instructions.
  • Resuming a continuation establishes a parent-child relationship that "aligns with the caller-callee relationship for standard function calls," so traps, exceptions, and embedder integration compose without special plumbing (Explainer, Asymmetric switching).
  • Continuations are one-shot (linear): invoking one more than once traps. This sidesteps stack-copying and GC of continuation objects, at the cost of not directly supporting multi-shot use-cases (backtracking, probabilistic programming).

For where this sits in the broader compilation story for algebraic effects, see Theory and Compilation; for how stack switching as a target relates to async I/O runtimes and event loops, see Effects and Event Loops.


Core Abstractions and Types ​

Continuation reference type ​

The single new reference type is the continuation, written in terms of an underlying function type $ft:

wat
;; proposals/stack-switching/Explainer.md — Instruction set extension
(type $ft (func (param t1*) (result t2*)))
(type $ct (cont $ft))     ;; continuation over $ft

The Explainer also uses the shorthand (cont [t1*] -> [t2*]). The parameter types t1* describe the stack shape required to resume/start the continuation; the result types t2* describe the stack shape once it runs to completion.

In the specification changes, cont <typeidx> is added as a new form of composite type (alongside func, struct, array), and two new heap types form a tiny lattice: nocont <: cont, where nocont is the bottom continuation type and cont the top. Continuations are explicitly not castable: the cast instructions ref.test / ref.cast / br_on_cast(_fail) gain a side-condition rt castable defined as not (rt <: (ref null cont)), so engines cannot inspect a continuation's concrete type at runtime.

Control tags ​

Tags are the coordination mechanism shared with exception handling. A tag declares both a parameter and a (now possibly non-empty) result type:

wat
;; proposals/stack-switching/Explainer.md — Generators
(tag $gen (param i32))          ;; suspend payload: i32 sent to handler; no value returned

;; proposals/stack-switching/Explainer.md — Extending the generator
(tag $gen (param i32) (result i32))  ;; suspend with i32; resumed with an i32 flag

The shorthand $t : [t1*] -> [t2*] says: when suspending on $t, the suspend site pushes values of type t1* to the handler, and expects values of type t2* back when resumed. A single tag may be used simultaneously by throw, suspend, and switch; the handler search only matches handlers of the same kind of event (Explainer, Execution).

The three relationships ​

  • Asymmetric switching (suspend/resume): resuming splices a suspended continuation onto the current one as a child; control returns to the parent's resume site when the child completes or suspends.
  • Symmetric switching (switch): a direct peer-to-peer transfer that combines "suspend current + resume peer" into a single stack switch — the engine avoids bouncing through the parent handler.
  • Partial application (cont.bind): pre-binds a prefix of a continuation's arguments, producing a new continuation of a narrower type. Because continuations are single-shot, no closure allocation is needed — slots for arguments are pre-allocated when the continuation is first created (Explainer, Producing continuations).

How Effects Are Declared ​

There are no "effect declarations" at the Wasm level — effects are encoded. A source-language effect operation becomes a control tag; performing the operation becomes a suspend (or switch) on that tag; the source handler becomes a resume with an (on $tag $label) clause that catches the suspension.

The Explainer's Specification changes give the precise validation rules. The seven instructions and their typing:

wast
;; proposals/stack-switching/Explainer.md — Instructions
cont.new $ct        : [(ref null $ft)] -> [(ref $ct)]            ;; $ct = cont $ft
cont.bind $ct $ct'  : [t1* (ref null $ct)] -> [(ref $ct')]       ;; bind a t1* prefix
suspend $e          : [t1*] -> [t2*]                             ;; $e : [t1*] -> [t2*]
resume $ct hdl*     : [t1* (ref null $ct)] -> [t2*]              ;; $ct = cont [t1*] -> [t2*]
resume_throw $ct $exn hdl*     : [te* (ref null $ct)] -> [t2*]   ;; $exn : [te*] -> []
resume_throw_ref $ct hdl*      : [exnref (ref null $ct)] -> [t2*]
switch $ct1 $e      : [t1* (ref null $ct1)] -> [t2*]             ;; see symmetric-switch typing

Handler clauses (hdl) have two shapes, defined in the binary format with a leading byte:

ClauseMeaningBinary tag
(on $e $l)Suspend handler: catching suspend $e branches to label $l0x00
(on $e switch)Switch handler: delimiter for switch on tag $e0x01

The new instructions occupy opcode space 0xe0–0xe6:

OpcodeInstruction
0xe0cont.new $ct
0xe1cont.bind $ct $ct'
0xe2suspend $t
0xe3resume $ct hdl*
0xe4resume_throw $ct $e hdl*
0xe5resume_throw_ref $ct hdl*
0xe6switch $ct1 $t

The validation rule for an (on $e $l) clause (Explainer, Instructions) is where the typing knot is tied: the label $l must expect the tag's parameter types t1* followed by a continuation reference (ref null? $ct), and that continuation's own parameter types must match the tag's result types t2*. In other words, the handler receives both the payload and a freshly delimited continuation whose resume-shape is exactly what suspend promised to consume.


How Handlers and Interpreters Work ​

Asymmetric: suspend / resume ​

resume both runs a continuation and acts as a delimiter: the suspended continuation it later produces captures execution "from the instruction immediately following suspend $e up to the resume instruction that handles $e" (Explainer, Generators). When the child runs suspend $e, control transfers to the innermost ancestor whose resume installed an (on $e ...) clause — directly analogous to exception-handler search, but the handler is additionally passed the reified continuation.

The canonical pattern, from the Explainer's generator example (proposals/stack-switching/Explainer.md; full module in examples/generator.wast):

wat
(func $consumer
  (local $c (ref $ct))
  (local.set $c (cont.new $ct (ref.func $generator)))
  (loop $loop
    (block $on_gen (result i32 (ref $ct))
      (resume $ct (on $gen $on_gen) (local.get $c))
      (return)                ;; $generator returned: no more data
    )
    ;; reached only via suspend: stack is [i32 (ref $ct)]
    (local.set $c)            ;; save the new (delimited) continuation
    (call $print)             ;; consume the yielded i32
    (br $loop)))

Handler semantics — "sheep" handlers ​

The original WasmFX / "typed continuations" design (the explainer linked from the WasmFX project site, preserved in the repo under proposals/stack-switching/design-notes/continuations/Explainer.md) names this hybrid sheep handlers: "The typed continuations proposal adopts a hybrid of shallow and deep handlers, which we call sheep handlers. Like a shallow handler, there is no automatic reinstallation of an existing handler. But like deep handlers a new handler is installed when a continuation is resumed." Concretely: a continuation handed to a handler is bare (no handler attached, unlike deep handlers), but a fresh handler is installed explicitly at each resume (unlike a raw shallow handler, where the consumer must re-wrap manually). This keeps the instruction set minimal while giving the programmer explicit control over handler installation.

Symmetric: switch ​

switch optimizes the common scheduler pattern where a suspend is immediately followed by the handler resuming a different continuation. Its typing makes the recursion explicit:

wast
;; proposals/stack-switching/Explainer.md — Instructions
switch $ct1 $e : [t1* (ref null $ct1)] -> [t2*]
  where:
  - $e   : [] -> [t*]
  - $ct1 = cont [t1* (ref null? $ct2)] -> [t*]   ;; peer also receives a (ref $ct2)
  - $ct2 = cont [t2*] -> [t*]                      ;; the just-suspended current continuation

switch suspends the current continuation (type $ct2), then directly resumes the peer ($ct1), implicitly passing itself as the peer's continuation argument so the peer can switch back. The matching handler is (on $e switch) — a switch handler that installs no suspend logic but acts as the delimiter for switch-suspended continuations. The Explainer's $scheduler2 (examples/scheduler2.wast) builds a cooperative scheduler where tasks switch directly to each other, requiring a recursive continuation type because each task receives a (ref null $ct) parameter.

Aborting: resume_throw / resume_throw_ref ​

To cancel a suspended continuation, resume_throw $ct $exn hdl* resumes it only to immediately raise exception $exn at the suspension point, unwinding it. Because a value is not actually delivered, the continuation's input types t1* are unconstrained. resume_throw_ref is identical but takes the exception as an exnref operand. The Explainer's task-cancellation example wraps resume_throw in a try_table so the abort exception is swallowed and the old continuation is deallocated (examples/scheduler2-throw.wast):

wat
;; proposals/stack-switching/Explainer.md — Canceling tasks
(block $exc_handler
  (try_table (catch $abort $exc_handler)
    (resume_throw $ct $abort (call $task_dequeue))))

How the reference interpreter realizes this ​

The OCaml reference interpreter under interpreter/ makes the semantics concrete and is the clearest source for the runtime model. In interpreter/exec/eval.ml:

ocaml
(* interpreter/exec/eval.ml — Administrative Expressions & Continuations *)
| Prompt of handle_table * code               (* an installed handler *)
| Suspending of tag_inst * value stack * (int32 * ref_) option * ctxt
and ctxt = code -> code                        (* a captured continuation = a context function *)
and handle_table = (tag_inst * idx) list * tag_inst list  (* (on $e $l)* , (on $e switch)* *)

type cont = int32 * ctxt
type ref_ += ContRef of cont option ref        (* the option ref enforces one-shot use *)

Three details are worth quoting because they ground claims that are easy to hand-wave:

  1. One-shot is enforced by a mutable option ref. Each invoker — resume, resume_throw, resume_throw_ref, switch, cont.bind — pattern-matches ContRef {contents = Some (...)} and then executes cont := None. Re-use hits ContRef {contents = None} and yields Trapping "continuation already consumed" (eval.ml, the Resume/ContBind/Switch cases). The spec's prose "destructively modify the suspended continuation such that any subsequent use will result in a trap" is literally this assignment.

  2. resume installs a Prompt; suspend produces a Suspending that bubbles up. Resume reduces to Prompt (hs, ctxt (args, [])). Suspend reduces to Suspending (tagt, args, None, fun code -> code). The Suspending administrative instruction then propagates outward through Label, Frame, and Handler frames, accumulating the surrounding context into ctxt — that accumulated ctxt becomes the captured continuation.

  3. The prompt is where the search terminates and the continuation is reified (eval.ml, ~line 1307):

    ocaml
    | Prompt ((hs, _), (vs', {it = Suspending (tagt, vs1, None, ctxt); _} :: es')), vs
      when List.mem_assq tagt hs ->
        let FuncT (_, ts) = func_type_of_tag_type c.frame.inst (Tag.type_of tagt) in
        let ctxt' code = compose (ctxt code) (vs', es') in
        [Ref (ContRef (ref (Some (Lib.List32.length ts, ctxt'))))] @ vs1 @ vs,
        [Plain (Br (List.assq tagt hs)) @@ e.at]

    When the bubbling Suspending reaches a Prompt whose handle-table contains the tag, the interpreter wraps the captured context ctxt' into a fresh ContRef, pushes it plus the payload vs1 onto the stack, and branches to the handler's label. A non-matching Prompt re-wraps the context and keeps the Suspending propagating (the | Prompt (hso, ... Suspending ...) -> ... Suspending (..., ctxt') case), faithfully implementing innermost-handler search. The switch case has its own Prompt ... (on ea switch) reduction that resumes the peer in place without unwinding to the handler.


Performance Approach ​

  • No whole-program transform. Compilers emit ordinary call-stack-shaped code plus suspend/resume; the engine owns the actual stack switch. This preserves natural stack structure for debuggers and profilers, unlike CPS or Asyncify.
  • One-shot, no copying. Linearity means an engine can implement a continuation as a real OS/segmented stack that is moved on suspend/resume rather than copied. Multi-shot would require stack copying or GC of cyclic continuation graphs; the proposal deliberately forgoes it.
  • switch collapses two stack switches into one. The asymmetric scheduler needs two switches per task hand-off (task→loop, loop→task); switch does it in one by transferring control peer-to-peer, "avoiding the need for an intermediate stack switch to the parent" (Explainer, Task scheduling).
  • cont.bind without allocation. Argument slots are pre-allocated at cont.new, so partial application reuses the existing continuation object instead of allocating a closure (Explainer, Producing continuations).
  • Engine work is ongoing. Implementation experience continues to be reported for Wasmtime-oriented tooling, e.g. the Continuing Stack Switching in Wasmtime (WAW 2025) session.

Composability Model ​

Continuations are explicitly composable: "when a suspended continuation is resumed it is spliced onto the current continuation," and resuming may itself be the top-level (main) stack (Explainer, Continuations). Because the parent-child relationship mirrors caller-callee, the substrate composes with the rest of Wasm:

  • With exceptions. Tags are shared with exception handling; aborting a continuation (resume_throw) raises a normal Wasm exception at its suspension point, caught by an ordinary try_table.
  • With traps and the embedder. Returning from a continuation transfers control to the instruction after its resume, exactly like a function return, so embedder calls and traps unwind naturally.
  • As a universal target. One mechanism serves many surface features:
Source featureExample languagesWasmFX encoding
Generators / iteratorsC#, JavaScript, Kotlin, Pythonyield tag; resume loop with (on $yield ...)
Async / awaitC#, Dart, JavaScript, Rust, Swiftasync/await/yield/fulfill tags; suspend/resume on I/O
CoroutinesC++, Kotlinsymmetric switch, or asymmetric suspend/resume
Lightweight threadsErlang, Go, Haskell, OCaml 5yield tag; scheduler resumes from a task queue
First-class continuationsHaskell, OCaml 5, Schemedirect mapping onto cont
Effect handlersKoka, OCaml 5, Effoperation → tag; handler → resume + (on ...)

The proposal's own examples/ directory ships runnable encodings: async-await.wast ($async/$await/$yield/$fulfill tags), generators.wast, lwt.wast/fun-lwt.wast (lightweight threads), actor.wast/actor-lwt.wast (actors), and pipes.wast/fun-pipes.wast (pipes). See Koka for a source effect system and Effects and Event Loops for how the async examples map onto real event loops.


Strengths ​

  • Strong theoretical grounding from the OOPSLA 2023 effect-handlers work (typed, sound formalization), now carried into a standards-track spec.
  • A single low-level control substrate that many source features reuse, instead of N bespoke mechanisms.
  • Better compilation target than mandatory whole-program transforms: smaller code, debugger-friendly stacks.
  • Minimal surface: one reference type, seven instructions, tags reused from exception handling.
  • No GC dependency — one-shot linearity avoids cyclic continuation graphs and stack copying.
  • Composes cleanly with exceptions, traps, and embedder integration via the parent-child / caller-callee alignment.
  • switch gives schedulers an efficient symmetric-transfer primitive.

Weaknesses ​

  • Not yet a finalized core standard: Phase 3 is active implementation, not the finish line; engine and toolchain coverage is still uneven.
  • One-shot continuations cannot directly express multi-shot effects (backtracking, nondeterminism, process duplication) — the Explainer acknowledges these use-cases are out of scope.
  • It is a substrate: language implementers still need substantial frontend/runtime integration to lower a high-level effect system onto tags + resume.
  • The typing of handler clauses and recursive continuation types ($scheduler2) is intricate; encoding correctness is easy to get subtly wrong.
  • WAT/encoding is verbose and unergonomic for humans (it is a compiler target, not a source syntax).

Key Design Decisions and Trade-offs ​

DecisionRationaleTrade-off
One mechanism (continuations) for all control flowAvoids N special-purpose features; one thing to spec, implement, and optimizeSource languages must do non-trivial lowering work themselves
One-shot (linear) continuationsNo stack copying, no GC of cyclic continuation graphs; engine can move stacksMulti-shot effects (backtracking, nondeterminism) not directly expressible
Reuse exception-handling tags, extended with resultsMinimal additions; resumable exceptions are a natural generalizationTag namespace shared across throw/suspend/switch; handler search must filter by kind
"Sheep" handlers (shallow install + explicit re-install at resume)Programmer controls handler lifetime; instruction set stays minimalMore verbose than deep handlers (no auto-reinstall); consumer manages the loop
Asymmetric (suspend/resume) as the coreParent-child mirrors caller-callee, so traps/exceptions/embedder compose for freeScheduler task hand-off costs two stack switches
Add symmetric switch as a separate instructionCollapses scheduler hand-off to one stack switchNeeds switch-handler delimiters and recursive continuation types
cont.bind for partial applicationReconciles Wasm's block-typing (all branches must agree on continuation type); no allocationExtra instruction; subtle prefix-binding typing
Continuations are non-castable (rt castable rule)Engine need not store/expose concrete continuation type identityNo runtime downcasting of continuations

Where the Current Spec Differs from the 2023 Paper ​

The current Stack-switching Explainer.md has evolved beyond the OOPSLA 2023 / WasmFX project site "typed continuations" explainer. Notable differences:

  • Handler-clause syntax changed from (tag $e $l) to (on $e $l), and a second clause shape (on $e switch) was added for symmetric switching.
  • switch is now a first-class instruction. The 2023 paper discussed switch / switch_to only as a potential extension under "design considerations"; it is now standardized with its own opcode (0xe6), typing rule, and (on $e switch) handler form. Symmetric switching is a headline feature of the current spec.
  • resume_throw_ref was added (opcode 0xe5), letting an abort raise an exception supplied as an exnref operand rather than only by tag immediate.
  • The barrier instruction was dropped. The original explainer defined barrier to trap any suspension crossing a boundary ("a catch-all handler that handles any control tag by immediately trapping"). It is not part of the current Explainer's instruction set.
  • Heap-type lattice and base spec. The current spec adds the nocont/cont heap types (nocont <: cont) and the non-castability side-condition, and rebases on Wasm 3.0 (function references + exception handling), reflecting how the surrounding standards moved since 2023.
  • "Sheep handler" terminology is de-emphasized in the standards Explainer (which describes resume as installing a handler and acting as a delimiter) even though the underlying semantics remain the hybrid the paper called sheep handlers.

The seven-instruction set (cont.new, cont.bind, suspend, resume, resume_throw, resume_throw_ref, switch) plus the cont reference type is the current authoritative surface; the OOPSLA paper's headline summary — "our extension is minimal and only adds three main instructions for creating, suspending, and resuming continuations" — reflects the earlier, smaller design (cont.new to create, suspend to suspend, resume to resume; resume_throw and cont.bind round out the original instruction set).


Relation to Other Wasm Proposals ​

ProposalRelationship to stack switching
Exception HandlingTags are reused and generalized with result types; resume_throw raises
Function ReferencesRequired by cont.new (continuations are created from typed funcrefs)
GCIndependent; one-shot continuations need no GC
JS Promise IntegrationAlternative async path; stack switching is more general
ThreadsOrthogonal; stack switching manages concurrency within one thread

Why It Matters for Algebraic Effects ​

From an effect-systems viewpoint, stack switching gives Wasm a practical backend for handler-based control: operations map to suspend sites, handlers map to resume with (on ...) clauses, and continuation capture/resume happens at the runtime substrate level rather than via source transforms. It does not impose one source-language effect system — it is a shared target for many. See Theory and Compilation for compilation strategies, Koka for a source effect language whose async lowers onto exactly these primitives, OCaml 5 for one-shot effect handlers that map almost directly, and Effects and Event Loops for how stack switching connects to async I/O runtimes and event loops. The corpus index, comparison, evolution, papers, and parallelism notes place WasmFX among the broader algebraic-effects landscape.


Sources ​