Setting up a local Wasm runtime for testing
This guide answers one question: how do you run and inspect a .wasm module on the command line — without a browser — so you can iterate on compile output, read trap messages, and confirm exports before wiring the module into a page?
Prerequisites
- [ ]
node18+ (ships nativeWebAssemblyand--experimental-wasm-modules). - [ ]
wasmtimeorwasmerfor a standalone, browser-independent runtime. - [ ]
wabt(wasm2wat,wasm-validate) to disassemble and sanity-check the binary. - [ ] A built
.wasmfile. Awasm32-unknown-unknownbuild runs anywhere; awasm32-wasibuild needs a WASI-capable host.
Step-by-step procedure
1. Install a standalone runtime
wasmtime is the reference WASI runtime and the quickest to install. Verify the binary before going further.
curl https://wasmtime.dev/install.sh -sSf | bash
exec "$SHELL" # reload PATH
wasmtime --version # e.g. wasmtime 21.0.0
Prefer wasmer if you need its broader embedding story:
curl https://get.wasmer.io -sSfL | sh
wasmer --version
2. Validate and disassemble first
Before running anything, confirm the binary is well-formed and see what it actually exports. A failed wasm-validate here saves you from chasing a runtime error that is really a build bug.
wasm-validate module.wasm && echo "valid"
wasm2wat module.wasm | grep '(export'
3. Run a WASI command module
If your module was built for wasm32-wasi and exports _start (a WASI command), wasmtime run executes it directly, wiring up stdout, args, and the clock.
wasmtime run module.wasm
# pass program args after the file:
wasmtime run module.wasm -- --iterations 1000
4. Call a single exported function
For a wasm32-unknown-unknown library module that exports plain functions (no _start), invoke one by name with --invoke.
# call `add(2, 3)` exported by the module
wasmtime run --invoke add module.wasm 2 3
5. Drive the module from node for browser-API parity
node’s WebAssembly object matches the browser’s, so it is the closest non-browser environment for testing the exact instantiation code your page runs. Use it when you need instantiate, an importObject, or to read linear memory.
// run.mjs — node --experimental-wasm-modules run.mjs
import { readFile } from "node:fs/promises";
const bytes = await readFile(new URL("./module.wasm", import.meta.url));
const importObject = {
env: { memory: new WebAssembly.Memory({ initial: 1 }) },
};
const { instance } = await WebAssembly.instantiate(bytes, importObject);
console.log("exports:", Object.keys(instance.exports));
console.log("add(2, 3) =", instance.exports.add(2, 3));
node --experimental-wasm-modules run.mjs
Expected output
A clean run of the node driver and a wasmtime --invoke call look like this:
$ node --experimental-wasm-modules run.mjs
exports: [ 'memory', 'add' ]
add(2, 3) = 5
$ wasmtime run --invoke add module.wasm 2 3
warning: using `--invoke` with a function that takes arguments is experimental
5
If the module traps instead, wasmtime prints the trap code and a backtrace (richer with WASMTIME_BACKTRACE_DETAILS=1):
Error: failed to invoke command default
Caused by:
wasm trap: out of bounds memory access
note: run with `WASMTIME_BACKTRACE_DETAILS=1` for more details
Gotchas
Error: failed to find function export 'add'. The export name in the binary differs from what you invoked. Rust mangles names and wasm-bindgen may rename them; run wasm2wat module.wasm | grep '(export' and use the exact string you see there.
Error: unknown import: 'wasi_snapshot_preview1::fd_write' has not been defined (running under plain node). The module is a WASI build but you instantiated it with a bare importObject that has no WASI shim. Run it with wasmtime run (which provides WASI), or import a node WASI polyfill (node:wasi) and pass its wasiImport namespace.
error: Validation error: SIMD support is not enabled. The runtime predates or disables a proposal your module uses. Upgrade the runtime, or enable the feature explicitly, e.g. wasmtime run -W simd module.wasm.
TypeError: WebAssembly.instantiate(): Import #0 module="env" error: memory import is required. Your importObject is missing a memory the module imports. Provide env.memory sized to at least the module’s declared minimum page count (each page is 64 KiB).
Performance note
A standalone runtime skips the browser’s JIT warm-up and DOM event loop, so a cold wasmtime run of a tiny module completes in single-digit milliseconds — fast enough to put in a --warmup-backed benchmark loop. But note the inverse: wasmtime’s default Cranelift tier is not V8’s optimizing tier, so absolute throughput numbers from the CLI are a relative signal for iteration, not a prediction of in-browser speed.
Frequently Asked Questions
Do I need WASI to run a module locally?
Only if the module imports WASI syscalls (fd_write, clock_time_get, etc.). A wasm32-unknown-unknown build with no host imports runs under wasmtime --invoke or node with an empty importObject. A wasm32-wasi build needs a WASI-aware host like wasmtime or wasmer.
Why does node need --experimental-wasm-modules?
That flag enables importing .wasm files as ES modules directly. If you instead read the bytes with fs and call WebAssembly.instantiate (as in step 5), you do not need the flag at all — the WebAssembly global is always available in node 18+.
Should I test in a local runtime or in the browser?
Both, for different reasons. Use wasmtime/wasmer for fast, deterministic iteration on compile output and trap analysis; use node for WebAssembly-API parity; then verify the real load path in a browser, where MIME, CORS, and instantiateStreaming behavior differ.
Related
- Wasm instantiation lifecycle — how
instantiateand theimportObjectbind, mirrored by the CLI runtimes. - How to decode wasm files manually — read the binary
wasm2watdisassembles. - Wasm binary format deep dive — the section layout
wasm-validatechecks.
← Back to Polyfill Alternatives & Fallbacks