Quick answer: Construct 3 For Each loops skip instances when you destroy objects mid-loop, when picking conditions inside the loop modify the SOL (Selected Objects List), or when the default unordered iteration encounters index shifts. Use For Each ordered by descending UID when destroying, and avoid re-picking the iterated object type inside the loop body.

You have 20 enemies on screen. You run a For Each loop to check their health and destroy the dead ones. But after the loop runs, some enemies that should have been processed were skipped entirely. The count does not add up. This is one of the most frustrating bugs in Construct 3 because the loop appears to work — it just silently misses instances without any error message.

The Symptom

You create a For Each event on an object type — say, Enemy. Inside the loop, you check a condition (like Enemy.Health ≤ 0) and destroy the matching instances. After the loop, you expect all dead enemies to be gone. But one or two remain. If you add a debug text object that counts iterations, you notice the loop ran fewer times than the total number of instances.

The problem gets worse when more instances meet the destruction condition. If you destroy every other instance, roughly half of the expected destroys are missed. If only one instance is destroyed per frame, the bug may not appear at all, making it intermittent and hard to reproduce.

Sometimes the symptom is different: instead of skipping destruction, the loop applies an effect (like adding score or spawning a particle) to fewer instances than expected. The root cause is the same — the loop is iterating over a list that is being modified while iteration is in progress.

What Causes This

There are three primary causes, and they all relate to how Construct 3 manages its internal instance lists during iteration:

1. Destroying instances inside a For Each loop. This is the most common cause. Construct 3 internally iterates over instances using an index. When you destroy instance at index 3, all subsequent instances shift down by one. The loop increments to index 4, but what was previously at index 4 is now at index 3. The instance that was originally at index 4 is never visited. For every instance you destroy, the instance immediately after it is skipped.

2. SOL (Selected Objects List) modification inside the loop. The SOL is Construct 3’s mechanism for tracking which instances are “picked” by conditions. A For Each loop iterates over the current SOL. If you place a condition inside the loop that re-picks the same object type (for example, checking overlap with another instance of the same type), Construct 3 may modify the SOL mid-iteration. The loop then operates on a different set of instances than it started with.

3. Unordered iteration assumptions. The standard For Each event does not guarantee a specific iteration order. Instances are visited in their internal array order, which can change when instances are created or destroyed. If your logic depends on processing instances in a specific sequence (for example, left-to-right or oldest-to-newest), the standard For Each may produce inconsistent results, especially when combined with instance creation or destruction within the same tick.

4. Nested For Each on the same object type. If you nest two For Each loops on the same object type, the inner loop’s SOL restoration can interfere with the outer loop’s iteration. This causes the outer loop to skip instances or repeat them depending on how the SOL is saved and restored between iterations.

The Fix

Step 1: Flag instances for deferred destruction. Instead of destroying instances inside the loop, set an instance variable to mark them, then destroy all flagged instances after the loop completes.

// Event sheet approach - deferred destruction

// Add an instance variable to Enemy: "MarkedForDeath" (Number, default 0)

// Event 1: Flag dead enemies
System > For Each Enemy
  Enemy: Health ≤ 0
    → Enemy: Set MarkedForDeath to 1

// Event 2: Destroy flagged enemies (runs after the loop)
Enemy: MarkedForDeath = 1
    → Enemy: Destroy

This approach ensures the For Each loop visits every instance because no instances are removed during iteration. The destruction happens in a separate event that runs after the loop has fully completed.

Step 2: Use For Each ordered by descending UID when you must destroy inline. If deferred destruction is not practical for your design, iterate in reverse order. When you remove an instance from the end of the list, the indices of unvisited instances (which are all at lower indices) are not affected.

// Event sheet approach - descending UID iteration

System > For Each Enemy ordered by Enemy.UID descending
  Enemy: Health ≤ 0
    → Enemy: DestroyTextDebug: Set text to "Destroyed UID: " & Enemy.UID

By iterating from the highest UID downward, each destruction only affects instances that have already been visited. The loop processes every instance exactly once.

Step 3: Isolate SOL modifications. If your loop logic needs to pick other instances of the same object type (for example, finding the nearest enemy to another enemy), use a different approach that does not modify the SOL of the iterated type.

// WRONG - re-picks Enemy inside the For Each
System > For Each Enemy
  Enemy: Is overlapping Enemy   // modifies Enemy SOL!Enemy: Destroy

// RIGHT - use instance variables to avoid SOL conflicts
System > For Each Enemy
    → Enemy: Set MyX to Enemy.XEnemy: Set MyY to Enemy.Y

// Then use a separate event with Pick by comparison
// to find overlapping instances by coordinate proximity

Step 4: Use the scripting API for complex iteration. When event sheet iteration gets unwieldy, the JavaScript scripting API gives you direct control over the instance list.

// Scripting API approach - full control over iteration
const enemies = runtime.objects.Enemy.getAllInstances();

// Iterate in reverse to safely destroy
for (let i = enemies.length - 1; i >= 0; i--) {
  const enemy = enemies[i];
  if (enemy.instVars.Health <= 0) {
    // Process the dead enemy (add score, spawn effect, etc.)
    runtime.objects.ExplosionFX.createInstance(
      "Game", enemy.x, enemy.y
    );
    enemy.destroy();
  }
}

Step 5: Add a debug counter to verify completeness. During development, always verify that your loop visits every instance by tracking the iteration count.

// Add a global variable: LoopCounter (Number, default 0)

System > Set LoopCounter to 0
System > For Each Enemy ordered by Enemy.UID descendingSystem: Add 1 to LoopCounter

// After the loop:
TextDebug: Set text to "Iterated: " & LoopCounter &
  " / Total: " & Enemy.Count

Why This Works

The core issue is that Construct 3 uses index-based iteration over a mutable list. When you remove an element from a list while iterating forward through it, every element after the removed one shifts to a lower index. The iterator then advances past what was the next element.

Deferred destruction solves this completely by separating the “decide what to remove” phase from the “actually remove it” phase. The loop runs on a stable list, and destruction happens only after iteration is complete.

Descending UID iteration works because when you iterate from high indices to low, removing the current element only shifts elements at higher indices — which have already been visited. The unvisited elements at lower indices remain in place.

SOL isolation prevents Construct 3’s picking system from changing which instances the loop is iterating over. When a condition inside a For Each modifies the SOL for the same object type, the engine can lose track of which instance was “current,” leading to skipped or repeated iterations.

"If you are destroying inside a loop and not iterating in reverse, you will eventually lose instances. It is not a question of if but when. Descending UID is the safest default for any For Each that modifies instance count."

Related Issues

If your platform game has objects falling through the ground after being spawned in a loop, see Fix: Construct 3 Platform Behavior Falling Through Platforms. For problems with saving and restoring dynamically created instances, check Fix: Construct 3 Save/Load System Not Restoring State. If your AJAX calls inside loops are failing, see Fix: Construct 3 AJAX Request CORS Error. And if particle effects spawned by loop actions are not appearing on mobile, see Fix: Construct 3 Particles Not Showing on Mobile.

Descending UID. Always descending UID when destroying in a loop.