Skip to content

Fix: WebAssembly (WASM) Not Working — Module Fails to Load, Memory Error, or JS Interop Broken

FixDevs ·

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 function

Or memory grows beyond expectations and crashes:

// RangeError: WebAssembly.Memory: could not allocate memory
// OR: RuntimeError: Out of bounds memory access

Or 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/wasminstantiateStreaming validates the Content-Type header. If the server returns application/octet-stream (common for static hosts), the streaming API fails. Use instantiate with a buffer instead.
  • Import object must match exactly — WASM modules declare their imports (functions the host must provide). If importObject doesn’t provide the exact module name and function name, instantiation fails with a LinkError.
  • 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 use memory.grow().
  • wasm-bindgen generates glue code — the _bg.wasm file and the _bg.js file 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 = true

Fix 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: trueSharedArrayBuffer 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.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles