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
- Use a
SetInstead of anArray: Collisions often generate multiple events across overlapping pairs. Storing pending removals in a JavaScriptSetautomatically prevents duplicate removal attempts for the same entity in a single frame. - Remove Attached Constraints First: If the destroyed body is connected via springs, pins, or joints, remove those constraints before or alongside the body to avoid dangling references in the constraint solver.
- Syncing with Game Objects: If you are pairing
Matter.js with a rendering framework (such as PixiJS or Three.js),
remove visual meshes and decouple physics references inside the same
afterUpdateloop to ensure graphical state stays synchronized with the physics world.