Why Avoid Mutating Arrays in Matter.js Callbacks

Mutating array lengths directly inside Matter.js event callbacks can destabilize your physics simulation, cause skipped collision checks, and trigger unexpected runtime errors. During events such as collisionStart, collisionActive, or beforeUpdate, the physics engine is actively traversing its internal lists of bodies, pairs, and constraints. Altering the size of these collections mid-iteration disrupts the engine's internal pointers and state tracking. To maintain simulation integrity and avoid silent failures, any operations that add or remove physics bodies should be deferred until the current update step completes.

How Matter.js Iterates Over Collections

Matter.js relies on deterministic loops to process physical interactions. In a typical frame, the engine performs several sequential phases:

  1. Broadphase and Narrowphase Detection: Finding potential and actual overlapping body pairs.
  2. Event Dispatching: Triggering callbacks like collisionStart with a list of active collision pairs.
  3. Constraint and Velocity Resolution: Calculating impulses and updating body positions.

When you listen to an event such as collisionStart, the engine passes an event object containing an array of collision pairs. The engine is either in the middle of looping through this collection or preparing to read from linked composite arrays immediately afterward.

The Consequences of Mutating Array Lengths Mid-Callback

Modifying array lengths inside a callback—either by directly splicing an array or calling Composite.remove(world, body)—introduces several critical problems:

The Deferred Mutation Pattern

The correct approach to adding or removing bodies during an event callback is to defer the mutation until the engine has finished resolving the current tick.

Instead of removing an entity immediately inside the callback, collect the target entities in a queue, then remove them during the afterUpdate event.

const bodiesToRemove = new Set();

// 1. Mark bodies for removal inside the collision callback
Matter.Events.on(engine, 'collisionStart', (event) => {
    event.pairs.forEach((pair) => {
        if (pair.bodyA.label === 'projectile' || pair.bodyB.label === 'projectile') {
            bodiesToRemove.add(pair.bodyA);
            bodiesToRemove.add(pair.bodyB);
        }
    });
});

// 2. Safely mutate the world array after the physics step completes
Matter.Events.on(engine, 'afterUpdate', () => {
    if (bodiesToRemove.size > 0) {
        bodiesToRemove.forEach((body) => {
            Matter.Composite.remove(engine.world, body);
        });
        bodiesToRemove.clear();
    }
});

Using a Set instead of an array for the removal queue automatically handles duplicates if a body is involved in multiple collisions during the same tick. Processing removals within afterUpdate guarantees that the physics engine has finished all loop iterations, position updates, and collision dispatches for the current frame, ensuring smooth and error-free execution.