Fix: WebAssembly (WASM) Not Working — Module Fails to Load, Memory Error, or JS Interop Broken
Part of: React & Frontend Errors
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 is a low-level execution target on top of the browser, and most “not working” symptoms come from one of four mismatches between how the module was built and how it’s being loaded.
The first mismatch is content type. WebAssembly.instantiateStreaming reads the response synchronously while it’s still being fetched, which means the browser validates the Content-Type header before parsing a single byte. If the server returns application/octet-stream (common for static hosts like GitHub Pages, some S3 configurations, and certain default nginx setups), the streaming API throws a TypeError. The fix is either to configure the server to send application/wasm or to fall back to instantiate with an ArrayBuffer from response.arrayBuffer().
The second mismatch is the import object. Every WASM module declares the imports it requires — memory, tables, globals, and host functions — and the JavaScript that instantiates the module must supply each one with the exact module name, item name, and shape. If the module declares (import "env" "malloc" (func ...)) and your importObject puts malloc at the top level, you get LinkError: Import #0 module="env" function="malloc" error: function import requires a callable. The error message tells you what’s missing — read it carefully.
The third mismatch is linear memory and async instantiation timing. WebAssembly memory is a flat byte array shared between the module and JavaScript. After memory.grow(), every existing typed array view becomes detached and must be recreated. Trying to use a stale view returns wrong values or zero. The other timing trap is calling exported functions before instantiation completes — await init() from wasm-bindgen is non-optional, and forgetting it produces a Cannot read properties of undefined because the export proxy hasn’t been wired up yet.
The fourth mismatch is bundler path resolution. Vite, Webpack, and Rollup all need plugins to handle .wasm files. Without vite-plugin-wasm or the equivalent, the bundler treats the file as an asset, returns a URL string, and your import gets a string instead of an instantiated module. The same applies to new URL('./foo.wasm', import.meta.url) — without bundler support, the URL is wrong in production builds even though it works in dev.
Diagnostic Timeline
Minute 0 — Module fails to load. Your first instinct is to rebuild. It rarely helps. Open devtools → Network and find the .wasm request. Check three things in order: status code (200?), Content-Type (application/wasm?), and the response size (matches your build output?). One of these is almost always the problem.
Minute 3 — Wrong MIME type. If Content-Type is application/octet-stream, configure the server. For nginx: types { application/wasm wasm; }. For Express: express.static.mime.define({ 'application/wasm': ['wasm'] }). For Cloudflare Pages, add a _headers file entry. Until you can fix the server, swap instantiateStreaming for instantiate(await response.arrayBuffer()) — it works regardless of MIME type, just a few ms slower.
Minute 8 — LinkError on instantiation. Run WebAssembly.Module.imports(module) and console.table(imports). The result lists every {module, name, kind} triple the module wants. Match them one-for-one in your importObject. The most common miss is a memory import the module expected you to provide; if so, create new WebAssembly.Memory({ initial: N }) and pass it under env.memory.
Minute 15 — Module loads but instance.exports.foo() is undefined. This is almost always the async-instantiation race for wasm-bindgen modules. You need await init() before any foo() call. If you’re using top-level await in a non-ESM context (Webpack CommonJS chunks), the import resolves before init completes — wrap the call site in an async IIFE.
Minute 22 — RangeError: WebAssembly.Memory: could not allocate memory. You hit the 4GB v8 memory cap or asked for more pages than the browser will give in one shot. Drop initial to a smaller number and rely on memory.grow() to expand on demand. Remember to recreate every Uint8Array/Float32Array view after grow() — the old ones are detached and silently return zero.
Minute 30 — import.meta.url resolves wrong in production. In Vite dev, new URL('./foo.wasm', import.meta.url) returns a working URL. In the production build, Vite rewrites it only if you have vite-plugin-wasm installed and the import is statically analyzable. If you constructed the URL dynamically, the asset never gets emitted. Install the plugin and use the static form: import wasmUrl from './foo.wasm?url'.
Minute 40 — Function returns the wrong number. Check the type widths. A Rust i64 becomes a JavaScript BigInt, not a number. A Rust u32 larger than 2^31 - 1 arrives as a negative i32. A Rust bool is an i32 (0 or 1). If you’re not using wasm-bindgen, type conversions are manual — inspect the .wat text format with wasm-tools print to confirm the signature.
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().
Stale Uint8Array view after memory.grow() — after growing memory, every typed array view created from the old ArrayBuffer is detached. Reads return zero, writes silently no-op, and console.log(view.byteLength) reports zero. Always recreate views: view = new Uint8Array(memory.buffer). Wrap the access in a helper that checks view.byteLength === 0 and rebuilds on demand.
WASM works in Chrome but throws in Safari — Safari was historically behind on bulk-memory operations, reference types, and SIMD. If your module uses bulk-memory (the default for wasm-bindgen 0.2.85+), set [package.metadata.wasm-pack.profile.release] with wasm-opt = ['--enable-bulk-memory'] and verify your minimum Safari version supports it. For older Safari, build with --no-default-features to avoid bulk-memory.
import.meta.url is undefined in a Web Worker — Workers loaded via classic script have no import.meta. To use new URL('./foo.wasm', import.meta.url) inside a worker, instantiate the worker as a module: new Worker(workerUrl, { type: 'module' }). Then import.meta.url resolves to the worker’s own URL.
For related performance and bundling issues, see Fix: JavaScript Heap Out of Memory, Fix: Web Worker Not Working, Fix: Vite Failed to Resolve Import, and Fix: Esbuild 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: 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.
Fix: Webpack Bundle Size Too Large — Reduce JavaScript Bundle for Faster Load Times
How to reduce Webpack bundle size — code splitting, tree shaking, dynamic imports, bundle analysis, moment.js replacement, lodash optimization, and production build configuration.