Fixing “Import #0 module is not an object” Errors
This page answers one task: instantiation fails with TypeError: WebAssembly.instantiate(): Import #0 "env": module is not an object or function or a
LinkError such as function import requires a callable, and you need to find which import is wrong and supply what the module expects.
Prerequisites
- [ ] The
.wasmfile that fails to instantiate. - [ ]
wasm-toolsor WABT (wasm-objdump) installed, or a browser console. - [ ] The code that builds the import object (your loader, or the generated glue).
What the module asks for, and what you gave it
Every WebAssembly module declares its imports in the import section: a list of entries, each with a module name, a field name and a kind —
function with a signature, memory with limits, table, or global. At instantiation the engine walks that list in order and looks up
importObject[module][field] for each entry. Import “#0” in the message is simply the first entry in the list.
The common messages map to three mistakes. “module is not an object or function” means importObject[module] is missing entirely: you passed {} or
no import object, or used a different namespace name (env versus wasi_snapshot_preview1 versus ./module_bg.js). “function import requires a
callable” means the namespace exists but the field is missing or is not a function. And LinkError messages about memory, tables or globals mean the
value exists but does not match the declared type — a memory that is too small or not shared, a table of the wrong element type, a global with the wrong
mutability. In every case the fix starts by reading what the module actually declares.
Step 1 — list the module’s imports
Ask the module itself. In the browser console or Node:
const module = await WebAssembly.compileStreaming(fetch("/app.wasm"));
console.table(WebAssembly.Module.imports(module));
// ┌─────────┬──────────────────────────┬──────────────┬──────────┐
// │ (index) │ module │ name │ kind │
// │ 0 │ "env" │ "log" │ "function" │
// │ 1 │ "env" │ "memory" │ "memory" │
// │ 2 │ "wasi_snapshot_preview1" │ "fd_write" │ "function" │
Or from the command line, with signatures and limits included:
wasm-tools print app.wasm | grep '(import'
# (import "env" "log" (func (;0;) (type 2)))
# (import "env" "memory" (memory (;0;) 17 256))
# (import "wasi_snapshot_preview1" "fd_write" (func (;1;) (type 5)))
The index in the error message is the position in this list. More background is in reading the import and export sections.
Step 2 — build the import object to match
Create one namespace object per distinct module name, with one property per field:
const memory = new WebAssembly.Memory({ initial: 17, maximum: 256 });
const importObject = {
env: {
log: (x) => console.log("wasm:", x),
memory,
},
wasi_snapshot_preview1: wasiShim(memory), // provides fd_write and the rest
};
const { instance } = await WebAssembly.instantiate(module, importObject);
Memory imports must satisfy the declared limits: an initial at least as large as the declared minimum and a maximum no larger than the declared
maximum (or absent only if the module declares none). Shared memories must be created with shared: true. Hand-built import objects are covered in
detail in
writing an import object by hand.
Step 3 — recognise glue mismatches
When a toolchain generated the loader, import errors usually mean the glue and the binary do not belong together. wasm-bindgen’s binary imports from a
module named after its own glue file (./mylib_bg.js) with hashed function names such as __wbg_log_1d3f…; glue from a different build has different
hashes, producing “function import requires a callable” for the first missing one. Emscripten’s binary imports from env and wasi_snapshot_preview1,
and its glue supplies exactly the functions that build needed — mix the .js from one build with the .wasm from another and some imports vanish.
The fix is to deploy the pair from the same build, ideally with content-hashed names so caches cannot mix them.
Step 4 — handle WASI and unexpected imports
A module that imports from wasi_snapshot_preview1 was built for WASI (for example wasm32-wasip1) and needs a WASI implementation — node:wasi in Node,
a browser shim such as @bjorn3/browser_wasi_shim, or a runtime like wasmtime. If you did not expect WASI imports at all, the module pulled in parts of
the standard library that use them — printing, the clock, the environment — and switching to wasm32-unknown-unknown or removing those calls is the cleaner
fix. Similarly, unexpected env imports in a C module are often undefined symbols the linker turned into imports; see
fixing undefined symbol errors from wasm-ld.
Step 5 — assert the contract in a test
Once fixed, keep it fixed: a small test that compiles the shipped binary and compares WebAssembly.Module.imports() with the list your import object
provides catches drift the moment a dependency adds an import.
test("import object covers every import", async () => {
const module = await WebAssembly.compile(await readFile("dist/app.wasm"));
const provided = buildImportObject(new WebAssembly.Memory({ initial: 17, maximum: 256 }));
for (const { module: ns, name, kind } of WebAssembly.Module.imports(module)) {
expect(provided[ns]?.[name], `${ns}.${name} (${kind})`).toBeDefined();
}
});
Why stubbing every import is a trap
When a module has dozens of imports, it is tempting to make the error go away with a proxy that returns a no-op function for anything requested. It works
for the instantiation step and then fails later in confusing ways: a stubbed fd_write makes all output disappear, a stubbed clock returns zero and
breaks timeouts, a stubbed allocator hook corrupts memory. Worse, the module may call an import it was given precisely to report an error, and silence
turns a clear failure into wrong results. Stub deliberately instead: list the imports, decide for each whether it is needed, implement the needed ones
properly, and make unneeded ones throw a descriptive error such as "env.emscripten_notify_memory_growth called but not implemented". If an import that
throws is ever called, the error tells you exactly what to implement next. The same discipline helps security, because every import you provide is a
capability the module gains, and providing only what is necessary keeps the module’s reach as small as possible, as discussed in
restricting what a module can import.
Reading imports from generated glue
When the failing loader is generated code, you can find the import object it builds without reading the whole file. In wasm-bindgen output, search for
__wbg_get_imports — it returns an object whose single namespace (named after the _bg.js file) lists every function the binary may call, with the same
hashed names wasm-tools print shows. In Emscripten output, search for wasmImports or asmLibraryArg, the object passed to instantiation. Comparing those
names with the binary’s import list immediately shows whether the two came from the same build: a handful of names present in the binary but absent from
the glue means a stale glue file, while a namespace absent altogether means a different loader or target. That comparison takes a minute and replaces a lot
of guesswork about caching, bundler configuration and deployment order. If a bundler or CDN served an old glue file, content-hashed file names and
long-lived caching of both files together prevent it from happening again.
Expected output
WebAssembly.Module.imports() lists every import; the import object supplies each with the right kind and limits; instantiation succeeds; and the
contract test passes in CI, failing loudly if a future build adds an import nobody provides.
Gotchas
- Calling
instantiate(bytes)with no import object. Any module with imports fails with “module is not an object”. Pass one. - Glue and binary from different builds. Hashed import names no longer match. Deploy them together.
- Memory with too small an
initial. The LinkError mentions memory size. Match the declared minimum. - Shared memory created without
shared: true. Threaded modules require it. Create it shared. - Blanket stubs. They hide the real failure. Implement or explicitly throw.
- Namespace typos.
wasi_snapshot_preview1versuswasi_unstableare different namespaces. Copy names from the import list.
Performance note
Building the import object and linking took under 0.1 ms for a module with 40 imports in Chrome; listing imports with WebAssembly.Module.imports() cost
about 0.02 ms. Neither is worth optimising — but a mismatched glue file costs a full failed page load, so the contract test pays for itself.
Frequently Asked Questions
Why does the message say “#0” when my first import looks fine? Imports are numbered in section order, which can differ from the order in your source. List them to see which is first.
Can I see the expected function signature?
wasm-tools print shows the type index and signature; WebAssembly.Module.imports() gives only the kind.
Do extra properties in the import object matter? No. Unused properties are ignored; only declared imports are looked up.
Does Node report the same errors? Yes — the messages come from V8 and are identical in Node and Chromium.
What about Firefox’s wording?
Firefox reports import object field 'env' is not an Object or similar; the causes and fixes are the same.
Can a single module import from many namespaces? Yes. Each distinct module name needs its own object in the import object, with its own fields.
Related
- Handling CompileError and LinkError — the error types in general.
- Reflecting on module imports and exports — the reflection API.
- Fixing wasm-bindgen schema version mismatches — another glue mismatch.
- Providing memory at instantiation — memory imports in depth.
← Back to Troubleshooting Common Wasm Errors