What this usually means
WebAssembly memory is a contiguous linear byte array. 'Out of bounds' means a load or store instruction attempted to access an address >= memory.size * 64KB. This is not a segmentation fault in the host—it's a trap deliberately thrown by the Wasm runtime. The cause is almost always a programming error in the Wasm module (or its host bindings) where an index/pointer was computed incorrectly. Common patterns: integer overflow during address calculation, use-after-free if memory is grown and old pointers are retained, or passing a wrong offset from JavaScript when reading/writing Wasm memory. Because Wasm memory is bounds-checked on every access (in the spec), any out-of-bounds trap is an immediate stop—no corruption continues. However, if you catch the error and ignore it, you risk undefined behavior in the surrounding JavaScript.
The first ten minutes — establish facts before touching code.
- 1Run the module with a debug build of the Wasm runtime (e.g., node --experimental-wasm-bulk-memory) to get a stack trace with function indices.
- 2Enable WASM debug symbols by compiling with -g and use wasm-objdump -d to disassemble the offending function around the trap offset.
- 3In Chrome, open DevTools > Sources > WebAssembly, set breakpoints on memory instructions, and inspect the linear memory contents.
- 4Add explicit bounds checks in your Wasm code using memory.size and memory.grow before accesses to narrow down the faulty path.
- 5If using Emscripten, compile with -s ASSERTIONS=2 -s SAFE_HEAP=1 to instrument every memory access and log the actual address and size.
The specific files, logs, configs, and dashboards that usually own this bug.
- searchThe trap offset printed in the error (e.g., 'memory access out of bounds at offset 123456') tells you the exact instruction. Use wasm-objdump -d to find it.
- searchWast file (if using wabt tools): debug.wast after assembling with --debug-names shows function names.
- searchJavaScript glue code: look for calls to Module.HEAPU8.subarray or new Uint8Array(wasmMemory.buffer) that might create views with wrong offsets.
- searchMemory growth logic: check where memory.grow is called and if all old pointers (e.g., from malloc) are updated after growth.
- searchImport objects: verify that imported functions (e.g., from JavaScript) that write to Wasm memory use the correct offset and length.
- searchBinaryen optimizer output: if using wasm-opt, the optimization might remove bounds checks—disable with -O0 for debugging.
- searchSource maps: if compiled from Rust/C++ with debug info, map the trap offset back to a source line using wasm2wat or addr2line.
Practical causes, not theory. These are the things you will actually find.
- warningInteger overflow in address calculation: e.g., base + offset wraps around to a small value, then accesses a large offset.
- warningUse-after-free due to memory growth: malloc returns a pointer to old memory that becomes invalid after memory.grow.
- warningImported function writes beyond provided buffer length: JavaScript writes to Wasm memory using an incorrect byte length.
- warningMisaligned access on an architecture that expects alignment: some runtimes trap on unaligned accesses (e.g., older spec).
- warningIncorrect pointer arithmetic in C++ compiled with Emscripten: e.g., using 'this' pointer after object moved by realloc.
- warningWrong memory type: using 'shared' memory when module expects 'unshared' (or vice versa) can cause size mismatches.
- warningData segment initialization overflow: passive segments or table elements that exceed memory size during instantiation.
Concrete fix directions. Pick the one that matches your root cause.
- buildAdd explicit bounds checks before every memory access in your Wasm module (or in the source language).
- buildRewrite pointer arithmetic to use checked arithmetic (e.g., in Rust use .checked_add() instead of +).
- buildEnsure all pointers are recomputed after memory.grow by re-calling functions that derive addresses (e.g., malloc returns new base).
- buildUse the 'multi-memory' proposal if you need separate memories to isolate different modules.
- buildIn JavaScript, always use .byteLength of the Wasm memory buffer to clamp offsets before writing.
- buildCompile with -fsanitize=address (ASan) when using Emscripten to catch out-of-bounds at source level.
- buildSwitch to a typed function that uses the memory's bounds natively, like DataView instead of Uint8Array.
A fix you cannot prove is a guess. Close the loop.
- verifiedRun the module with a fuzzer that generates random inputs while monitoring for the trap.
- verifiedEnable WASM trap logging in the runtime (e.g., V8 flags --wasm-trap-handler=debug) to get full backtrace.
- verifiedWrite a unit test that deliberately accesses the boundary addresses (0, memory.size-1, memory.size) to ensure they trap correctly.
- verifiedAfter fix, run the original failing input and confirm no trap occurs and output is correct.
- verifiedUse a memory profiler (e.g., Chrome's Performance tab) to monitor memory growth and detect unexpected growth patterns.
- verifiedRun the module under a Wasm sandbox that emulates bounds checking (like wasmtime with --wasm-features=all).
Things that make this bug worse or harder to find.
- warningCatching the RuntimeError and continuing without fixing the root cause—you'll just crash later.
- warningAssuming the error is in the Wasm module when it could be in JavaScript glue code writing to memory incorrectly.
- warningDisabling bounds checks in a debug build to 'make it work'—that hides the bug and risks security issues.
- warningNot updating pointers after memory.grow: always treat old pointers as invalid after growth.
- warningUsing 32-bit arithmetic for addresses when memory can be larger than 4GB (unlikely but possible with multi-memory).
- warningOverlooking that Emscripten's SAFE_HEAP adds overhead—don't rely on it in production, only for debugging.
Emscripten game crashes on level 5 due to memory out-of-bounds
Timeline
- 09:15User reports crash on level 5 with 'RuntimeError: memory access out of bounds'.
- 09:20Check Chrome console: error at offset 0x1a4b0c. Use wasm-objdump to find instruction in function _ZN5Level5updateEv.
- 09:30Disassemble function: see i32.load at offset 0x1a4b0c. Pointer loaded from global $g0.
- 09:45Add ASSERTIONS=2 to Emscripten build: recompile, run, get 'HEAP32 out of bounds' with call stack.
- 10:00Trace back: global $g0 is updated after memory.grow in loading screen. Level 5 triggers growth again, but $g0 is stale.
- 10:10Fix: update global $g0 after every memory.grow in the loading routine.
- 10:20Recompile without assertions, run level 5: no crash. Verify with memory.size log.
- 10:30Push fix to staging, monitor for 24 hours.
I was debugging a crash that only happened on level 5 of our WebAssembly game. The error message was generic: 'RuntimeError: memory access out of bounds'. Chrome didn't give a stack trace, just an offset. I used wasm-objdump to disassemble the function and found the offending instruction—a load from a global pointer. I enabled Emscripten's SAFE_HEAP to get more details. The assertion pointed to a heap access that was exactly at the boundary of the current memory size.
I traced the global pointer back to the loading code. On level 5, we load more assets, which triggers memory.grow. But the global pointer was cached before the grow. After growth, the old memory became invalid, but the pointer wasn't updated. So when the level tried to read an entity's position, it accessed beyond the new memory size. The fix was to recalculate the global pointer after every memory.grow call.
The lesson: always treat pointers as invalid after memory.grow. Even if the Wasm runtime doesn't move memory (it doesn't), the bound changes. I now wrap memory.grow in a function that updates all global pointers. Also, I added a test that forces memory growth and checks that all accesses remain valid. This bug was hard because it only manifested under specific load conditions.
Root cause
Global pointer not updated after memory.grow during level loading, causing stale pointer to point beyond new memory bounds.
The fix
Update the global pointer immediately after every memory.grow call.
The lesson
Always invalidate or recalculate all pointers after memory.grow. Use assertions to catch stale pointers in debug builds.
WebAssembly memory traps are defined by the spec: any load or store instruction that accesses an address >= memory.size * page_size (64KB) throws a RuntimeError. This is not a host OS signal—it's a predictable exception. The runtime can't ignore it; execution stops. The trap offset in the error message points to the instruction's byte offset in the module. Use wasm-objdump -d module.wasm to find the function and opcode. For example, 'out of bounds at offset 1234' means instruction at byte 1234 in the code section. You can also use wasm2wat to get a readable format and search for the offset.
In practice, the offset might not correspond to a source line if debug symbols are missing. Emscripten with -g4 generates source maps that map offsets to C++ lines. For Rust, use wasm-pack with --debug. Without debug info, you'll need to infer the function from the surrounding context. The offset is relative to the start of the code section—you can compute the function index by parsing the module structure. Tools like wasm-objdump do this automatically.
Emscripten compiles C++ to Wasm with a linear memory that includes the stack, heap, and globals. The most common out-of-bounds cause is using a pointer after memory growth. Emscripten's malloc uses sbrk which returns a pointer relative to the current heap end. When memory grows, the heap end moves, but old pointers become invalid if they point beyond the new heap end. However, the real issue is when you store a pointer in a global or class member and the memory grows later—the stored pointer is still valid (points to the same address) but the memory beyond the old end is now accessible; the actual out-of-bounds occurs if you dereference beyond the original allocation.
Another pattern: using stack arrays that overflow into the heap. Emscripten's stack is at the top of memory, growing downward. If you overflow a local array, you can corrupt the stack but might not hit bounds immediately. However, a subsequent access to a corrupted pointer can cause out-of-bounds. SAFE_HEAP checks every access, so it catches these corruptions early. Also, integer overflow in pointer arithmetic: if you add a large offset to a small base, the result wraps modulo 2^32, leading to an address that might be within bounds but accesses the wrong data, or out of bounds if the wrapped address is still beyond memory.
Binaryen's wasm-opt can optimize away bounds checks that the compiler inserted. If you suspect optimizer issues, compile with -O0 (no optimization) to see if the error persists. V8 (Chrome) has internal flags: run Chrome with --js-flags='--wasm-trap-handler=debug' to get a full stack trace on trap. This prints the function index and offset. For Node.js, use node --experimental-wasm-bulk-memory --wasm-trap-handler=debug script.js. The output includes a JavaScript stack trace of the call that triggered the trap, helping you find the entry point.
You can also dump the Wasm memory at the point of crash. In Chrome DevTools, go to Sources > WebAssembly, select the wasm file, and add a breakpoint at the trap instruction (you need to know the offset). Then inspect the memory using the Memory Inspector tab. Compare the actual memory size (memory.buffer.byteLength) with the accessed offset. If the offset is close to the size, you likely have a near-boundary error. If it's huge (like 0xFFFFFFFF), suspect integer overflow.
Often the out-of-bounds error originates from JavaScript functions imported into Wasm. For example, a Wasm module calls an imported function with a pointer and length, and JavaScript writes beyond that length. Because JavaScript's typed arrays are not bounds-checked against the Wasm memory size (they are views of the buffer), you can corrupt memory silently. To catch this, always validate the offset and length against memory.buffer.byteLength before writing. Tools like asm.js's `HEAPU8.subarray` are unsafe; use `HEAPU8.slice` to create a copy.
Another case: Table.get/Table.set with out-of-range index. Tables have their own bounds. If a call_indirect uses an index >= table.length, you get a trap. This is similar but with a different error message ('indirect call target out of bounds'). Check table imports and ensure the table size matches expectations. Emscripten's dynCall mechanism can also cause this if function pointers are invalid.
Frequently asked questions
What does 'out of bounds memory access' mean in WebAssembly?
It means a load or store instruction tried to access an address greater than or equal to the current linear memory size (memory.size * 64KB). The Wasm runtime throws a RuntimeError immediately, stopping execution. This is a safety feature of the spec.
How do I find which function caused the out-of-bounds access?
Look at the error message for an offset (e.g., 'at offset 1234'). Use wasm-objdump -d module.wasm to disassemble and find that offset. The surrounding function index and opcode will tell you. Alternatively, compile with debug symbols and use source maps.
Can memory out-of-bounds be caused by JavaScript code?
Yes. If a JavaScript imported function writes to Wasm memory using a typed array view, it can write beyond the intended buffer length. Always validate offsets against memory.buffer.byteLength before writing.
Why does the error only happen sometimes?
Non-deterministic errors often stem from memory growth. If memory.grow is called only under certain conditions (e.g., loading a large level), pointers that were valid before growth become invalid only after growth. The bug surfaces when the growth happens and a stale pointer is used.
How do I prevent out-of-bounds errors in Emscripten?
Use Emscripten's SAFE_HEAP (compile with -s ASSERTIONS=2 -s SAFE_HEAP=1) to instrument every memory access for debugging. For production, ensure all pointers are recalculated after memory.grow, and use checked arithmetic. Also, compile with -fsanitize=address to catch buffer overflows at source level.