Fix: WebAssembly (WASM) Not Working — Module Fails to Load, Memory Error, or JS Interop Broken
Quick Answer
How to fix WebAssembly issues — instantiateStreaming vs instantiate, CORS for WASM files, linear memory limits, wasm-bindgen JS interop, imports/exports mismatch, and WASM in bundlers.
The Problem
WebAssembly.instantiateStreaming fails with a type error:
const result = await WebAssembly.instantiateStreaming(
fetch('/module.wasm'),
importObject
);
// TypeError: Failed to execute 'instantiateStreaming'
// Response has incorrect MIME type. Expected 'application/wasm'Or the WASM module loads but calling an exported function throws:
const { instance } = await WebAssembly.instantiate(wasmBuffer, importObject);
instance.exports.myFunction(42);
// RuntimeError: unreachable
// OR: TypeError: instance.exports.myFunction is not a functionOr memory grows beyond expectations and crashes:
// RangeError: WebAssembly.Memory: could not allocate memory
// OR: RuntimeError: Out of bounds memory accessOr wasm-bindgen generated bindings fail to import:
import init, { greet } from './pkg/my_wasm.js';
await init();
greet('World');
// Error: Cannot find module './pkg/my_wasm_bg.wasm'Why This Happens
WebAssembly has strict requirements that differ from regular JavaScript:
- MIME type must be
application/wasm—instantiateStreamingvalidates the Content-Type header. If the server returnsapplication/octet-stream(common for static hosts), the streaming API fails. Useinstantiatewith a buffer instead. - Import object must match exactly — WASM modules declare their imports (functions the host must provide). If
importObjectdoesn’t provide the exact module name and function name, instantiation fails with aLinkError. - Linear memory is fixed at instantiation — WebAssembly.Memory is a flat byte array. Out-of-bounds access causes
RuntimeError: Out of bounds memory access. You must allocate enough memory upfront or usememory.grow(). - wasm-bindgen generates glue code — the
_bg.wasmfile and the_bg.jsfile must be served together. Missing either breaks the binding.
Fix 1: Load WASM Correctly
Use the right loading strategy based on your environment:
// Method 1: instantiateStreaming (preferred — fastest)
// Requires server to send Content-Type: application/wasm
async function loadWasm(url, importObject = {}) {
try {
const result = await WebAssembly.instantiateStreaming(
fetch(url),
importObject
);
return result.instance;
} catch (e) {
if (e instanceof TypeError) {
// Fall back to instantiate if MIME type is wrong
console.warn('Streaming failed, falling back to ArrayBuffer');
return loadWasmFallback(url, importObject);
}
throw e;
}
}
// Method 2: instantiate with ArrayBuffer (works regardless of MIME type)
async function loadWasmFallback(url, importObject = {}) {
const response = await fetch(url);
const buffer = await response.arrayBuffer();
const result = await WebAssembly.instantiate(buffer, importObject);
return result.instance;
}
// Usage
const instance = await loadWasm('/module.wasm', {
env: {
memory: new WebAssembly.Memory({ initial: 256 }), // 256 pages = 16MB
abort: (msg, file, line, col) => {
throw new Error(`WASM abort: ${msg}`);
},
},
});Fix server MIME type (nginx):
# /etc/nginx/mime.types or server config
types {
application/wasm wasm;
}
# Or in server block
location ~* \.wasm$ {
add_header Content-Type application/wasm;
}Fix server MIME type (Express/Node.js):
import express from 'express';
import path from 'path';
const app = express();
// Register WASM MIME type before static middleware
express.static.mime.define({ 'application/wasm': ['wasm'] });
app.use(express.static('public'));Vite — WASM files handled automatically:
// vite.config.ts
import { defineConfig } from 'vite';
import wasm from 'vite-plugin-wasm'; // npm install vite-plugin-wasm
import topLevelAwait from 'vite-plugin-top-level-await';
export default defineConfig({
plugins: [wasm(), topLevelAwait()],
});
// Then import WASM directly
import init, { myFunction } from './my_module.wasm?init';
await init();
myFunction(42);Fix 2: Match the Import Object Exactly
The importObject must provide all imports declared by the WASM module:
// Inspect what a WASM module imports using the WebAssembly API
async function inspectWasmImports(url) {
const response = await fetch(url);
const buffer = await response.arrayBuffer();
const module = await WebAssembly.compile(buffer);
const imports = WebAssembly.Module.imports(module);
console.table(imports);
// [{module: "env", name: "memory", kind: "memory"},
// {module: "env", name: "puts", kind: "function"},
// ...]
}
// Build the import object to match
const importObject = {
env: {
// Memory (if required)
memory: new WebAssembly.Memory({
initial: 256, // 256 pages × 64KB = 16MB
maximum: 1024, // Optional maximum
}),
// Functions the WASM module calls into JS
puts: (ptr) => {
// Read a null-terminated string from WASM memory
const memory = importObject.env.memory;
const view = new Uint8Array(memory.buffer);
let str = '';
let i = ptr;
while (view[i] !== 0) str += String.fromCharCode(view[i++]);
console.log(str);
},
abort: () => { throw new Error('WASM aborted'); },
// Math functions often needed
'Math.sqrt': Math.sqrt,
'Math.log': Math.log,
},
// Some modules use 'wasi_snapshot_preview1' for WASI
wasi_snapshot_preview1: {
fd_write: (fd, iovsPtr, iovsLen, nwrittenPtr) => {
// Minimal WASI fd_write implementation
return 0;
},
proc_exit: (code) => { throw new Error(`WASI exit: ${code}`); },
},
};Fix 3: Manage WebAssembly Memory
Linear memory requires careful management, especially for strings and complex data:
// Allocate and work with WASM memory
const memory = new WebAssembly.Memory({ initial: 16 }); // 1MB
const instance = await loadWasm('/module.wasm', {
env: { memory }
});
const { exports } = instance;
// Pass a string to WASM
function passStringToWasm(str) {
const encoder = new TextEncoder();
const encoded = encoder.encode(str + '\0'); // Null terminate
// Allocate memory in WASM (module must export an allocator)
const ptr = exports.malloc(encoded.length);
// Write to WASM memory
const view = new Uint8Array(memory.buffer);
view.set(encoded, ptr);
return ptr;
}
// Read a string from WASM
function readStringFromWasm(ptr) {
const view = new Uint8Array(memory.buffer);
let end = ptr;
while (view[end] !== 0) end++; // Find null terminator
const bytes = view.subarray(ptr, end);
return new TextDecoder().decode(bytes);
}
// Grow memory when needed
function ensureMemory(requiredBytes) {
const currentBytes = memory.buffer.byteLength;
if (currentBytes >= requiredBytes) return;
const pagesNeeded = Math.ceil((requiredBytes - currentBytes) / 65536);
memory.grow(pagesNeeded);
// Note: After grow(), existing views (Uint8Array etc.) become invalid!
// Always re-create views after grow()
}
// Always re-create views after memory.grow()
let memView = new Uint8Array(memory.buffer);
function getMemView() {
if (memView.byteLength === 0) {
// Memory was grown, view is detached — recreate it
memView = new Uint8Array(memory.buffer);
}
return memView;
}Fix 4: Fix wasm-bindgen (Rust + WASM)
wasm-bindgen generates JavaScript bindings for Rust WASM modules:
# Build Rust WASM with wasm-pack
cargo install wasm-pack
wasm-pack build --target web # For browsers with ESM
wasm-pack build --target bundler # For webpack/vite
wasm-pack build --target nodejs # For Node.js
# Output structure (--target web):
# pkg/
# my_crate.js ← JS glue code
# my_crate_bg.wasm ← Compiled WASM binary
# my_crate_bg.js ← Auto-generated bindings
# my_crate.d.ts ← TypeScript types
# package.json// Browser ESM (--target web)
import init, { greet, MyStruct } from './pkg/my_crate.js';
// IMPORTANT: always await init() before using exports
const wasm = await init('./pkg/my_crate_bg.wasm'); // Path to .wasm file
greet('World'); // Calls Rust function
// Using structs
const instance = MyStruct.new(42);
console.log(instance.value());
instance.free(); // Manually free when done — avoid memory leaks
// With Vite (--target bundler)
import init, { greet } from './pkg/my_crate.js';
// Vite's wasm plugin resolves the .wasm path automatically
await init();
greet('World');// src/lib.rs — Rust side
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn greet(name: &str) -> String {
format!("Hello, {}!", name)
}
#[wasm_bindgen]
pub struct MyStruct {
value: i32,
}
#[wasm_bindgen]
impl MyStruct {
pub fn new(value: i32) -> Self {
Self { value }
}
pub fn value(&self) -> i32 {
self.value
}
pub fn compute(&self, factor: i32) -> i32 {
self.value * factor
}
}Enable console_error_panic_hook for better error messages:
// src/lib.rs
use wasm_bindgen::prelude::*;
#[wasm_bindgen(start)]
pub fn main() {
// Panics show in the browser console as readable messages
console_error_panic_hook::set_once();
}# Cargo.toml
[dependencies]
wasm-bindgen = "0.2"
console_error_panic_hook = "0.1"
js-sys = "0.3" # For JS types
web-sys = { version = "0.3", features = ["console", "Window", "Document"] }
[profile.release]
opt-level = 'z' # Optimize for size
lto = trueFix 5: WASM in Node.js
// Node.js 18+ — native WASM support, no special setup
import { readFile } from 'fs/promises';
// Load from filesystem (no fetch needed in Node.js)
const wasmBuffer = await readFile('./module.wasm');
const { instance } = await WebAssembly.instantiate(wasmBuffer, importObject);
// Node.js + wasm-pack (--target nodejs)
import { greet } from './pkg/my_crate.js';
// No init() needed for Node.js target — synchronous loading
greet('Node.js');
// Streaming compile in Node.js (for larger WASM files)
import { createReadStream } from 'fs';
async function loadWasmStreaming(filePath) {
// Node.js doesn't have fetch — simulate with a Response-like object
const response = new Response(createReadStream(filePath), {
headers: { 'Content-Type': 'application/wasm' },
});
const result = await WebAssembly.instantiateStreaming(response, importObject);
return result.instance;
}Fix 6: Performance and Optimization
// Compile once, instantiate multiple times (more efficient)
const wasmBuffer = await fetch('/module.wasm').then(r => r.arrayBuffer());
const wasmModule = await WebAssembly.compile(wasmBuffer);
// Create multiple instances (e.g., for worker threads)
const instance1 = await WebAssembly.instantiate(wasmModule, importObject);
const instance2 = await WebAssembly.instantiate(wasmModule, importObject);
// Share memory between WASM instances and JavaScript
const sharedMemory = new WebAssembly.Memory({
initial: 256,
maximum: 4096,
shared: true, // SharedArrayBuffer — requires COOP/COEP headers
});
// Pass large arrays efficiently (avoid copying)
function processLargeArray(data) {
const { malloc, free, process_array } = instance.exports;
const memory = new Uint8Array(instance.exports.memory.buffer);
// Allocate WASM memory
const ptr = malloc(data.length * 4); // 4 bytes per float32
// Write data directly to WASM memory (no copy if same ArrayBuffer)
const wasmArray = new Float32Array(instance.exports.memory.buffer, ptr, data.length);
wasmArray.set(data);
// Process in WASM
process_array(ptr, data.length);
// Read result
const result = wasmArray.slice();
// Free memory
free(ptr);
return result;
}Measure WASM performance:
// Compare JS vs WASM performance
const data = new Float32Array(1000000).fill(1.5);
console.time('JS');
const jsResult = data.map(x => Math.sqrt(x));
console.timeEnd('JS');
console.time('WASM');
const wasmResult = processLargeArray(data); // Your WASM implementation
console.timeEnd('WASM');Still Not Working?
SharedArrayBuffer is undefined despite shared: true — SharedArrayBuffer requires cross-origin isolation headers (COOP: same-origin and COEP: require-corp). Without these headers, SharedArrayBuffer is undefined and shared: true in WebAssembly.Memory throws. Configure your server to send these headers and verify with self.crossOriginIsolated === true.
WASM instantiation succeeds but functions return wrong values — Rust/C data types don’t map 1:1 to JavaScript. For example, a Rust i64 becomes a JavaScript BigInt (not a number), and Rust booleans may be represented as i32 (0 or 1). Use wasm-bindgen for Rust to handle type conversion automatically, or carefully check type mappings in the WASM text format (.wat file).
WASM module is too large for the browser — a 5MB+ WASM file causes noticeable load delays. Optimize with: wasm-opt -Oz module.wasm -o module.opt.wasm (from the binaryen toolkit), enable gzip/brotli compression on the server, and lazy-load WASM only when needed with dynamic import().
For related performance issues, see Fix: JavaScript Heap Out of Memory and Fix: Web Worker Not Working.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: React useTransition Not Working — UI Still Freezes, isPending Never True, or Transition Not Deferred
How to fix React useTransition and startTransition issues — what counts as a transition, Suspense integration, concurrent rendering requirements, and common mistakes that prevent transitions from deferring.
Fix: Web Worker Not Working — postMessage Ignored, Cannot Import Module, or Worker Crashes Silently
How to fix Web Worker issues — postMessage data cloning, module workers, error handling, SharedArrayBuffer setup, Comlink, and common reasons workers silently fail.
Fix: React Warning — Each Child in a List Should Have a Unique key Prop
How to fix React's missing key prop warning — why keys matter for reconciliation, choosing stable keys, avoiding index as key pitfalls, keys in fragments, and performance impact.
Fix: Svelte Store Subscription Leak — Memory Leak from Unsubscribed Stores
How to fix Svelte store subscription memory leaks — auto-subscription with $, manual unsubscribe, derived store cleanup, custom store lifecycle, and SvelteKit SSR store handling.