# WebAssembly

## What it is

WebAssembly, usually shortened to Wasm, is a binary format for executable code.
Its core specification calls it “a safe, portable, low-level code format
designed for efficient execution and compact representation”, and describes it
as a virtual instruction set: the instructions of an imagined processor, which a
browser or another host first checks and then translates into machine code for
the real one. The short form is a contraction, not an acronym, which is why the
specification writes Wasm and not WASM.

A `.wasm` file is usually the output of a compiler for some other language. The
main goal, in the specification's own account, is fast applications on the Web,
but the format makes no assumptions about the Web, so it can also run as a
stand-alone virtual machine or inside other programs. The standard is layered to
match: the core specification defines the instructions, the formats, validation
and execution, and no particular environment. Separate documents define the
JavaScript API that compiles, links and calls modules, and the Web API that
loads them over the network.

The first version became a W3C Recommendation on 5 December 2019. Version 2.0,
essentially finished three years before its successor, added instructions that
work on vectors, bulk operations on memory, functions with more than one return
value, and a simple first form of reference types. On 17 September 2025 the
project announced version 3.0 as the standard now in force, and said it was
already shipping in most major browsers. The same announcement names Java,
OCaml, Scala, Kotlin, Scheme and Dart among the languages that now target Wasm
through its garbage collector.

The vocabulary is small. The unit of code is a module: it holds functions,
tables, memories and global variables, imports what it needs, each import named
by a module name and an item name, and exports what it offers under names of its
own. There are four number types, 32-bit and 64-bit integers and floating-point
numbers, written `i32`, `i64`, `f32` and `f64`, plus a single 128-bit vector
type and opaque references. Integers carry no sign of their own; each operation
reads them as signed or unsigned, and a 32-bit integer also serves as a Boolean
and as a memory address. Instructions take their operands from an implicit stack
and leave their results on it, and control flow is structured: blocks, loops and
conditionals nest, and a branch can only target one of them.

Data lives in a linear memory, one continuous, writable array of bytes that
starts at a declared size and can grow while the program runs. A load or a store
outside its current size does not reach anything else. It traps, which stops
execution at once, and a trap cannot be handled inside Wasm, only reported to
the host around it. The example below uses the text format the specification
defines for people to read.

## Why it matters

Speed is the first reason. The design goals ask for performance close to native
code and a format that a single fast pass can decode, validate and compile,
whether the compiler runs just in time or ahead of time. The binary is meant to
be more compact than both text and native machine code, and it is streamable:
work can begin before the last byte has arrived. The binary format even keeps
each function's type declaration in a different section from its body, so that
the bodies can be compiled in parallel and while they stream in. A team that
already owns a well-tested library in another language can compile it for the
browser instead of rewriting it in JavaScript.

Portability is the second. The same module runs on any modern processor, desktop
or mobile, and in any host that embeds an engine: a browser, a stand-alone
runtime or another program. The specification favours no particular language,
programming model or object model, so the format is a common target rather than
one language's private runtime.

Containment is the third. A module has no ambient access to the computer it runs
on. Files, the network, the page and the clock are out of reach unless the host
hands it a function for them as an import, so the host decides exactly what the
code may touch. On the Web, the Web API notes that Wasm can run wherever
JavaScript can and has essentially the same threat model, and cannot expose more
than JavaScript already could. Version 3.0 widened what fits inside that
boundary: memories that can be addressed with 64 bits, limited on the web to 16
gigabytes, several memories in one module, a garbage collector for languages
that expect one, and exceptions that no longer have to escape to the host, which
the announcement calls neither portable nor efficient.

## What it looks like in practice

Here is a module in the text format, with a single exported function that adds
two 32-bit integers:

```wat
(module
  (func (export "add") (param i32 i32) (result i32)
    local.get 0
    local.get 1
    i32.add))
```

The function's type says it takes two `i32` values and returns one. The body is
three instructions on the stack: `local.get 0` pushes the first parameter,
`local.get 1` pushes the second, and `i32.add` pops both and pushes their sum,
which is left on the stack as the result. `(export "add")` gives the function
the name the outside world sees. Converted to the binary format, the file starts
with the four bytes `0x00 0x61 0x73 0x6D`, a zero followed by the letters asm,
and then the binary format's version, which is still 1.

In a browser, the JavaScript API loads it:

```js
const url = "/add.wasm";
const { instance } = await WebAssembly.instantiateStreaming(fetch(url));
instance.exports.add(2, 3); // 5
instance.exports.add(2147483647, 1); // -2147483648
```

`instantiateStreaming` compiles the module while the response is still arriving
and resolves to the compiled module and an instance of it. The instance's
exports are ordinary JavaScript functions. The second call shows what the types
mean: integer addition in Wasm wraps around modulo 2 to the power of 32, and the
JavaScript API reads an `i32` result as signed, so one more than the largest
positive value comes back as the most negative one. Nothing is thrown. A trap
would be, though: if a function reads outside its memory, the call ends in a
`WebAssembly.RuntimeError`.

A module that needs something from its host says so. With
`(import "env" "log" (func (param i32)))` in the module, instantiation needs a
second argument, `{ env: { log: console.log } }`, and the module can call `log`
like any of its own functions. Leave the second argument out and instantiation
fails with a `TypeError`; pass an `env` object without a callable `log` and it
fails with a `WebAssembly.LinkError`, in both cases before any of the module's
code has run. Imports and exports are the whole boundary, and every value that
crosses it is one of a few plain types; an address in the module's memory is
just an `i32`.

[A portfolio entry on building a native macOS client in Swift 6](/portfolio/built-a-native-macos-client-in-swift-6/)
describes the other way of reusing code from another language. In that work,
carried out in August 2026, a Rust core was linked into a Swift application as a
static archive and reached through its C interface, which the entry treats as
the contract. A thin wrapper owns every pointer and every string that crosses
the boundary, and everything the interface returns has an ownership rule, where
a mistake means a leak or a crash with no compiler warning. No Wasm is involved
there, which is what makes the comparison useful: the same problem of a narrow
boundary between two languages, solved with native linking instead of an import
list and a sandboxed memory.

## Where it goes wrong

**The promise rejects before anything compiles.** A missing `Content-Type`, or
any value other than `application/wasm` with case ignored, fails with a
`TypeError`, and the specification notes that extra parameters are not allowed,
not even an empty `application/wasm;`. A status that is not ok fails the same
way, and so does a response from another origin that CORS does not make
same-origin. Fix the server's media type and headers. `WebAssembly.instantiate`
on bytes you have fetched yourself skips the media type check but loses the
streaming compilation.

**A view of memory goes dead.** JavaScript sees a module's memory through
`memory.buffer`, and the arrays built on it. When the memory grows, the
JavaScript API detaches a fixed-length buffer and replaces it with a new one, so
arrays built on the old buffer are cut off from the memory. This happens after
`memory.grow()` called from JavaScript and also after a `memory.grow`
instruction inside the module, during a call that looks harmless from outside.
Create views from `memory.buffer` again after any call that may grow the memory,
rather than keeping one for the lifetime of the page.

**Expecting Wasm to reach the page by itself.** A module has no ambient access
to anything, so every effect on the page, the network or storage goes through a
function JavaScript passes in as an import. Types bite at the same boundary: an
`i64` crosses as a `BigInt`, not a number, and a function with a `v128`
parameter or result throws a `TypeError` each time JavaScript calls it. Design
the import and export list as an interface, with plain numbers on it, and keep
vectors on the Wasm side.

**Taking the sandbox for a safe program.** The sandbox protects the host from
the module. It does not protect the module from itself. The specification states
that it cannot stop an unsafe language compiled to Wasm from corrupting its own
memory layout inside linear memory: a buffer overflow in compiled code
overwrites the module's own data, and only an access past the end of the whole
memory traps. Keep the source language's own checks and tests, and treat data
the module receives from outside as untrusted input.

**Shipping the binary without protecting it.** The registration of
`application/wasm` says the format includes no integrity or privacy protection,
and that where such protection is needed it must come from outside, through
HTTPS for example. As with JavaScript, some ways of loading and running it are
limited by Content Security Policy or by the same-origin policy. And because
Wasm becomes machine code on real hardware, the core specification warns that it
is potentially open to side-channel attacks at the hardware level. Serve modules
over HTTPS, and guard who may change a module as closely as who may change a
script.

## Further reading

- [WebAssembly Specification](https://webassembly.github.io/spec/core/)
  — the core specification for version 3.0: structure, validation, execution and
  the binary and text formats.
- [Introduction](https://webassembly.github.io/spec/core/intro/introduction.html)
  — the definition, the design goals, the scope of each document and the
  security considerations.
- [WebAssembly JavaScript Interface](https://webassembly.github.io/spec/js-api/)
  — compiling and instantiating from JavaScript, how values cross, `Memory.grow`
  and the three error classes.
- [WebAssembly Web API](https://webassembly.github.io/spec/web-api/)
  — `instantiateStreaming`, the checks on the response and the
  `application/wasm` registration.
- [Wasm 3.0 Completed](https://webassembly.org/news/2025-09-17-wasm-3.0/)
  — the announcement of 17 September 2025 and what version 3.0 adds.
- [WebAssembly Core Specification 1.0](https://www.w3.org/TR/wasm-core-1/)
  — the W3C Recommendation of 5 December 2019.

<https://engineer.company/glossary/webassembly/>
