Defer Entity Destruction in Matter.js

Removing bodies from a Matter.js simulation directly inside collision callbacks often causes null reference exceptions and broken physics calculations. When an entity is removed mid-tick, the engine may still attempt to resolve constraints, update broadphase pairs, or run subsequent collision steps on the now-deleted reference. This article explains why these errors occur and details how to implement a deferred destruction queue using Matter.js lifecycle events to safely dispose of bodies.

Why Direct Destruction Fails

Matter.js operates in discrete steps executed by Engine.update(). During a single tick, the engine performs collision detection, fires collision events, updates collision pairs, and resolves velocities and positions.

If you invoke Composite.remove(engine.world, body) directly inside a collision listener (such as collisionStart), you mutate the underlying arrays while the engine is actively iterating over them. This causes subsequent internal loops—such as pair separation or constraint solving—to encounter undefined or null pointers, typically throwing errors such as:

TypeError: Cannot read properties of undefined (reading 'position')

The Solution: Deferred Destruction

To prevent these errors, separate the detection of an entity’s destruction from its actual removal from the physics world. Flag entities for deletion or append them to a disposal queue during collision events, and only remove them once the engine completes its calculations for that frame.

Implementing an afterUpdate Queue

The safest place to execute physics removals is inside the afterUpdate event, which fires immediately after Engine.update() completes all internal physics iterations.

import Matter from 'matter-js';

const { Engine, World, Bodies, Events, Composite } = Matter;

const engine = Engine.create();
const destructionQueue = new Set();

// Helper to flag a body for deletion
function destroyBody(body) {
    destructionQueue.add(body);
}

// 1. Queue bodies during collision events
Events.on(engine, 'collisionStart', (event) => {
    event.pairs.forEach((pair) => {
        const { bodyA, bodyB } = pair;

        // Example condition: destroy projectile upon hitting a target
        if (bodyA.label === 'projectile' && bodyB.label === 'target') {
            destroyBody(bodyA);
        }
    });
});

// 2. Process the queue after the physics update finishes
Events.on(engine, 'afterUpdate', () => {
    if (destructionQueue.size === 0) return;

    destructionQueue.forEach((body) => {
        // Clean up associated constraints, if any
        const constraints = Composite.allConstraints(engine.world).filter(
            (c) => c.bodyA === body || c.bodyB === body
        );
        constraints.forEach((constraint) => {
            Composite.remove(engine.world, constraint);
        });

        // Remove the body from the world
        Composite.remove(engine.world, body);
    });

    // Clear the queue for the next frame
    destructionQueue.clear();
});

Key Considerations