Fix Ammo.js Maximum Call Stack Size Exceeded Error
The “maximum call stack size exceeded” error in ammo.js typically occurs when the physics engine is pushed into an infinite recursion loop, most commonly triggered by passing invalid delta time values to the simulation step or executing recursive collision callbacks. This article explains the primary causes of this stack overflow error in the Emscripten-ported Bullet physics library and provides direct, actionable solutions to resolve it.
The Primary Cause: Uncontrolled Substepping
The most common trigger for this error is how developers pass elapsed
time (delta time) to the ammo.js physics world stepper
function:
physicsWorld.stepSimulation(timeStep, maxSubSteps, fixedTimeStep);If the browser tab loses focus, the delta time
(timeStep) accumulates. When the tab is refocused, a
massive delta time value is passed to the engine. If
maxSubSteps is set to a high value (or not properly
constrained), ammo.js attempts to run hundreds of substeps
in a single frame to catch up. Because ammo.js is compiled
via Emscripten, these heavy internal iterations quickly overflow the
WebAssembly/JavaScript call stack.
How to Fix It
To prevent this, you must clamp the delta time before passing it to
stepSimulation. This limits the maximum number of substeps
the engine will attempt to process in a single frame.
// Calculate your delta time (seconds)
let deltaTime = clock.getDelta();
// Clamp delta time to a maximum threshold (e.g., 1/10th of a second)
const maxDeltaTime = 0.1;
deltaTime = Math.min(deltaTime, maxDeltaTime);
// Step the simulation safely
physicsWorld.stepSimulation(deltaTime, 10);Secondary Cause: Recursive Collision Callbacks
Another frequent cause is modifying physics states directly inside collision callback loops. If you register a contact listener and attempt to immediately add, remove, or drastically alter rigid bodies during the physics tick, it can trigger an infinite loop of collision updates.
Since ammo.js resolves collisions recursively, altering
the physical world mid-callback disrupts the solver’s stack memory.
How to Fix It
Defer any physical modifications (such as destroying objects or
changing collision flags) to the end of the frame, outside the
stepSimulation call stack.
// Instead of destroying a body inside the callback:
function onCollisionDetect(body1, body2) {
// Queue the removal instead of executing it immediately
removalQueue.push(body1);
}
// In your main animation loop:
physicsWorld.stepSimulation(deltaTime, 10);
processRemovalQueue(); // Safely remove bodies after the physics step completesThird Cause: Passing NaN or Invalid Arguments
Passing a value that evaluates to NaN (Not a Number) or
undefined as the timeStep will break the
internal loop counters in the compiled C++ code. The engine’s stepping
loop fails to increment correctly, resulting in an immediate stack
overflow.
Ensure your clock implementation always returns a valid, positive float:
if (isNaN(deltaTime) || deltaTime <= 0) {
deltaTime = 1 / 60; // Fallback to a default 60Hz frame time
}