Mutating an Array While Iterating Over It
When I worked on a snake game at the company, I ran into a classic bug that was hard to spot in a large codebase: mutating an array while iterating over it. Here’s the example:
roomData.players.forEach((player) => {
console.log(...roomData.players);
console.log(player.id);
console.log("[DEBUG]: " + roomData.players.length);
this.killPlayer(player.id, DeathReason.TimesUp);
});
Output:
app-1 | { id: 'o-YtjCYy050TW9H0AAAB', type: 'snake', avatar: 'snake4' } { id: 'swzaRwreukn6J--NAAAD', type: 'human', avatar: 'human1' }
app-1 | o-YtjCYy050TW9H0AAAB
app-1 | [DEBUG]: 2
What happened
In this example, I’m iterating through each player in roomData. The first console.log shows 2 players, and roomData.players.length also shows 2, but console.log(player.id); only runs once.
Why it happens
The reason is that killPlayer runs while I’m still iterating, and it removes a player from roomData. So even though the array started with 2 players, one gets removed during the loop, which is why only one log appears.
This is a classic bug you can run into when deleting elements from an array while iterating over it.
How to solve it
There is usually more than one way to solve a problem, unless the problem is just boolean, I mean binary. :D
Solution 1: iterate backwards
Iterate from the end of the array back to 0. The length changes every time an element is deleted, but the index still moves safely backward.
Example:
for (let i = roomData.players.length - 1; i >= 0; i--) {
const player = roomData.players[i];
console.log(...roomData.players);
console.log(player);
console.log("[DEBUG]: " + roomData.players.length);
this.killPlayer(player.id, DeathReason.TimesUp);
}
Output:
{ id: 'hLxAlYSUHVmryFyyAAAD', type: 'snake', avatar: 'snake4' } { id: 'Zw6lzr0fQW-rQQ3tAAAF', type: 'human', avatar: 'human2' } { id: 'Dpq0vcWR1C6jwYKvAAAJ', type: 'human', avatar: 'human1' }
app-1 | { id: 'Dpq0vcWR1C6jwYKvAAAJ', type: 'human', avatar: 'human1' }
app-1 | [DEBUG]: 3
app-1 | [DEATH] Game: 1766979246579-jdwemtzz8xf, Socket: Dpq0vcWR1C6jwYKvAAAJ, Player: human1, Reason: timesUp, Position: (615, 435)
app-1 | [DISCONNECT] Player "human1" with socket Dpq0vcWR1C6jwYKvAAAJ disconnected in room 1766979246579-jdwemtzz8xf.
app-1 | { id: 'hLxAlYSUHVmryFyyAAAD', type: 'snake', avatar: 'snake4' } { id: 'Zw6lzr0fQW-rQQ3tAAAF', type: 'human', avatar: 'human2' }
app-1 | { id: 'Zw6lzr0fQW-rQQ3tAAAF', type: 'human', avatar: 'human2' }
app-1 | [DEBUG]: 2
app-1 | [DEATH] Game: 1766979246579-jdwemtzz8xf, Socket: Zw6lzr0fQW-rQQ3tAAAF, Player: human2, Reason: timesUp, Position: (585, 255)
app-1 | [DISCONNECT] Player "human2" with socket Zw6lzr0fQW-rQQ3tAAAF disconnected in room 1766979246579-jdwemtzz8xf.
app-1 | { id: 'hLxAlYSUHVmryFyyAAAD', type: 'snake', avatar: 'snake4' }
app-1 | { id: 'hLxAlYSUHVmryFyyAAAD', type: 'snake', avatar: 'snake4' }
app-1 | [DEBUG]: 1
app-1 | [DEATH] Game: 1766979246579-jdwemtzz8xf, Socket: hLxAlYSUHVmryFyyAAAD, Player: snake4, Reason: timesUp, Body Positions: (525, 345), (495, 345), (465, 345)
app-1 | [DISCONNECT] Player "snake4" with socket hLxAlYSUHVmryFyyAAAD disconnected in room 1766979246579-jdwemtzz8xf.
With this approach, the array starts with 3 elements and gets trimmed from the end down to 0. The downside is that the code is a little harder to read.
Why it works
If you iterate from 0 to the end, the array length changes as items are removed, so some elements get skipped. When you start from the last element, delete it, then move to i - 1, the indexes stay valid.
Good
This approach does not need a copy of the array.
Solution 2: iterate over a copy
Create a copy of the data and iterate over that copy while modifying the original array. If you only need IDs, you can copy just the identifiers.
In the example below, this.games[gameId].players holds the player IDs from roomData.players, and we use those IDs to delete items from roomData.
Object.entries(this.games[gameId].players).forEach(([player]) => {
console.log(...roomData.players);
console.log(player);
console.log("[DEBUG]: " + roomData.players.length);
this.killPlayer(player, DeathReason.TimesUp);
});
Output: similar to solution 1
{ id: '0CqNmgn663afDdaJAAAB', type: 'snake', avatar: 'snake1' } { id: 'Ub-jLHWBB_3mA5uWAAAD', type: 'human', avatar: 'human1' } { id: 'P6tNeqCfyuP_im7SAAAH', type: 'human', avatar: 'human2' }
app-1 | 0CqNmgn663afDdaJAAAB
app-1 | [DEBUG]: 3
app-1 | [DEATH] Game: 1766979497017-8yxtctnvbmx, Socket: 0CqNmgn663afDdaJAAAB, Player: snake1, Reason: timesUp, Body Positions: (675, 465), (675, 495)
app-1 | [DISCONNECT] Player "snake1" with socket 0CqNmgn663afDdaJAAAB disconnected in room 1766979497017-8yxtctnvbmx.
app-1 | { id: 'Ub-jLHWBB_3mA5uWAAAD', type: 'snake', avatar: 'snake2' } { id: 'P6tNeqCfyuP_im7SAAAH', type: 'human', avatar: 'human2' }
app-1 | Ub-jLHWBB_3mA5uWAAAD
app-1 | [DEBUG]: 2
app-1 | [DEATH] Game: 1766979497017-8yxtctnvbmx, Socket: Ub-jLHWBB_3mA5uWAAAD, Player: human1, Reason: timesUp, Position: (345, 255)
app-1 | [DISCONNECT] Player "human1" with socket Ub-jLHWBB_3mA5uWAAAD disconnected in room 1766979497017-8yxtctnvbmx.
app-1 | { id: 'P6tNeqCfyuP_im7SAAAH', type: 'snake', avatar: 'snake6' }
app-1 | P6tNeqCfyuP_im7SAAAH
app-1 | [DEBUG]: 1
app-1 | [DEATH] Game: 1766979497017-8yxtctnvbmx, Socket: P6tNeqCfyuP_im7SAAAH, Player: human2, Reason: timesUp, Position: (645, 285)
app-1 | [DISCONNECT] Player "human2" with socket P6tNeqCfyuP_im7SAAAH disconnected in room 1766979497017-8yxtctnvbmx.
Good
This is easier to read because you work with a separate copy of the old array data.
Limitation
You need an extra variable, so there’s a bit more handling.
End
This is the end of the post. If you spot any mistakes, feel free to leave feedback through the portfolio footer. I hope this helps. :D