GC and Exceptions in Wasmtime: What Changed in 47
Picture this: you’re bringing an existing language (with real objects and real exceptions) to WebAssembly. Everything compiles… until the runtime story gets weird.
Maybe your compiler starts sprinkling extra “glue” code everywhere, or you notice generated .wasm files getting bloated. Maybe exceptions are encoded as “return values plus a tag,” which means every call site carries overhead even when nothing goes wrong.
That pain has been shrinking. Wasmtime 47 (released 2026-07-20) enabled both the Wasm GC and Wasm exception-handling proposals by default. (github.com) The most interesting part isn’t just “feature enabled.” It’s how Wasmtime had to change internally—especially the garbage collector—to make these proposals practical.
Why Wasm GC and exceptions existed as proposals in the first place
Early WebAssembly (often called “MVP” for “minimum viable product”) was intentionally minimal. It doesn’t natively understand “objects” and “references” the way most high-level languages do. Instead, it mostly deals in numbers and raw memory-like access.
So, when languages using an objects-and-references data model (think JavaScript, many functional runtimes, and most OO languages) wanted to target WebAssembly, they often had to bring their own garbage collector inside the module. (bytecodealliance.org) That approach makes binaries larger and pushes lots of traditionally GC-related machinery (like root finding) into native code—where it’s harder to optimize and harder to get right.
Meanwhile, exception-using languages faced a similar mismatch. Without a dedicated exceptions mechanism in WebAssembly, toolchains typically needed custom calling conventions: every function call returns both “a value” and “whether it threw.” Every call site must branch on that status. (bytecodealliance.org) That means overhead on the common path (normal returns), not just on the rare path (actual failures).
So the proposals—Wasm GC and Wasm exceptions—exist to make WebAssembly a better compilation target for languages that already have these runtime concepts.
What Wasm GC actually adds (beyond “a GC exists”)
The Wasm GC proposal extends the WebAssembly language with constructs for defining types of heap-allocated data, like struct and array, and with subtyping relationships between them. (bytecodealliance.org) In plain terms:
- A
structis like an object record: named fields stored together. - An
arrayis like a dynamically sized sequence with elements. - Subtyping means you can treat one reference type as another when the type system allows it.
Most importantly for language implementers, the Wasm program doesn’t need to explicitly manage object lifetimes. The runtime does the garbage collection and allocation bookkeeping. (bytecodealliance.org) That removes the incentive to embed a separate GC in every .wasm binary.
A quick “feel” for the new instructions
Wasm GC introduces instructions such as:
struct.new $typeto allocate a new instancestruct.get $type $fieldto read a fieldstruct.set $type $fieldto write a field (bytecodealliance.org)
And you can see the “reference world” in types like (ref null $node): a reference that may either be null or point at a node. (bytecodealliance.org)
Here’s a simplified sketch (adapted from the general idea):
(rec
(type $node (struct
(field $key (mut f64))
(field $left (mut (ref null $node)))
(field $right (mut (ref null $node)))
))
)
Even if this WAT (WebAssembly Text format) feels unfamiliar, the core takeaway is simple: the module now describes heap data shapes and reference behavior, instead of pretending everything is raw memory.
How Wasmtime’s GC works: Cheney-style semi-space copying
With the proposal enabled, Wasmtime needs a real garbage collector implementation. Wasmtime uses a Cheney-style semi-space copying collector. (bytecodealliance.org)
Let’s translate that into a beginner-friendly mental model.
Two halves of the heap
The GC heap is split into two equal regions (“semi-spaces”):
- Active semi-space: where new allocations happen.
- Idle semi-space: where existing objects currently live, waiting to be copied.
When a collection happens, live objects are copied from the idle semi-space into the active one (or, more precisely, to the “new active” space), and references are updated to point to the new locations. (bytecodealliance.org)
GC roots: where the collector starts
A GC root is any place the runtime can find references to heap objects from. Examples include references in registers, globals, or—here—active references inside Wasm stack frames. (bytecodealliance.org)
Copying collectors work by tracing from the roots: follow references, copy what you reach, then keep going until you’ve reached everything reachable.
Allocation is a bump pointer
During normal execution, allocation is implemented as a bump pointer: a cursor that moves forward in memory as objects are allocated, without per-object bookkeeping. (bytecodealliance.org)
This matters because it keeps allocation fast and makes the collector’s “normal path” lightweight.
No read/write barriers (yet)
Many modern moving GCs use read/write barriers—small hooks inserted around loads/stores—to keep metadata consistent. Wasmtime’s collector “does not require any read or write barriers” per the design described for this implementation. (bytecodealliance.org)
That’s a major simplification: fewer moving parts to keep correct.
The clever part: GC heap inside Wasm linear memory
A core runtime detail: Wasmtime’s GC heap is implemented by reusing WebAssembly linear memories under the hood. (bytecodealliance.org)
A linear memory is Wasm’s “big byte array” abstraction—an addressable region the runtime manages with bounds and sandboxing.
Instead of storing GC references as native host pointers, Wasmtime stores them as 32-bit indices into the GC heap’s linear memory. (bytecodealliance.org) In other words:
- A GC “reference” is not a raw pointer.
- It’s an index (a number) that the runtime interprets safely.
Why index-based references are a big deal
This design has benefits in three dimensions:
- Safety: even if the collector has a bug that corrupts the heap, a malicious Wasm module can’t escape the sandbox to access host memory. (bytecodealliance.org)
- Speed: because it’s integrated with linear memory machinery, it can use virtual-memory techniques like guard pages to reduce explicit bounds checking. (bytecodealliance.org)
- Portability: managing memory allocation/reset behavior is already handled in Wasm’s linear-memory implementation across platforms, so building the GC heap on top of it inherits that portability. (bytecodealliance.org)
Wasmtime also notes practical performance effects: 32-bit GC references are more compact on 64-bit hosts and integrate with allocation mechanisms to preserve fast instantiation (it cites “5-microsecond instantiation times” as a target impacted by allocator integration). (bytecodealliance.org)
Catching bugs with fuzzing (because GC correctness is unforgiving)
Garbage collectors are notorious for bugs that show up late: use-after-free, object graph corruption, and misoptimized paths that only break under weird shapes of data.
So Wasmtime extended and augmented its fuzzing strategy:
wasm-smithwas extended to generate Wasm GC programs. (bytecodealliance.org)- Because generating “interesting enough” GC programs can take a long time, additional fuzzers were added:
- one to exercise complex object graphs, type references, and subtyping relationships
- another to detect heap corruption caused by collector bugs or compiler misoptimizations (bytecodealliance.org)
That combination is exactly what you want for a tracing collector: explore structure, not just instructions.
Wasm exceptions: why the proposals matter to performance
Now shift to exceptions.
Without an exceptions mechanism in the WebAssembly language, toolchains often emulate exceptions via special return conventions: function results plus an “exception or not” flag. (bytecodealliance.org)
That forces every caller to:
- check the status
- branch to a handler path
Even when you never throw, the code footprint and control-flow pressure still exist.
With Wasm exceptions, the language provides constructs like throw and try/catch style handling. (bytecodealliance.org) The runtime can implement exceptions using classic unwinding.
What unwinding means
In exception handling, unwinding is the process of walking back up the call stack after an exception is thrown, skipping normal returns until reaching a handler that can catch the exception.
Wasmtime’s design goal here is crucial: it claims “zero overhead to the common, normal-return call paths,” replacing the per-call checking that emulations require. (bytecodealliance.org)
Where GC and exceptions collide in real runtimes
GC and exceptions don’t live in separate universes. Unwinding control flow can cross stack frames, and stack frames may contain live references.
Wasmtime 47’s release notes include a related correctness detail: uncaught Wasm exceptions at component boundaries are turned into traps so one component can’t catch exceptions from another. (github.com) That’s the kind of boundary-policy behavior that matters when “exception flow” becomes a first-class runtime concept.
The takeaway isn’t a single implementation detail—it’s that making exceptions “real” in Wasm forces the runtime to reason carefully about stack state, types, and isolation.
What performance to expect (honestly)
A lot of engineering announcements sound like they expect instant parity with long-tuned JavaScript engines. Wasmtime’s own write-up is more grounded.
It says the early engineering focus has been correctness of the collector rather than throughput/latency. The collector is brand new and hasn’t had the decades of performance iteration seen in engines like V8 and SpiderMonkey. (bytecodealliance.org)
It also describes a workload-shaped design choice: Wasmtime is primarily tuned for production patterns where many small, disposable Wasm instances get created, run a small task set, and get dropped; the overall system scales horizontally with lots of instances rather than relying on a single long-lived process. (bytecodealliance.org)
So the right expectation is that Wasm GC and exceptions are now usable by default, but performance maturation is still part of the journey.
Closing the loop: proposals enabled by default is the milestone
Wasm GC and exception-handling didn’t just add syntax. They forced Wasmtime to grow a real runtime story for tracing objects and unwinding exceptional control flow—grounded in a concrete collector (Cheney-style copying), safe reference representations (32-bit indices into linear memory), and correctness work (fuzzing object graphs to find corruption). (bytecodealliance.org)
And the most practical signal is also the simplest: Wasmtime 47 turned both proposals on by default, so language toolchains can stop treating GC and exceptions as “special cases” that need custom calling conventions and embedded runtimes. (github.com)
That’s why this release feels like more than a checkbox. It’s the runtime catching up to the kind of programs modern languages want to express on WebAssembly.
Comments (0)
No comments yet. Be the first to respond!
Leave a Comment
Your comment will be visible after review.