You've asked
Is it safe to delete entries within a forEach loop of a javascipt map?
but also said
Problem is, that the condition in the loop is async/callback based, so it would be cumbersome to do deferred deletion.
(Thank you Thilo for pointing that out when I missed it!)
That means you're not deleting during the forEach
. Those callbacks won't happen until later, after the forEach
has completed. E.g., they're already deferred. So the question "Is it safe to delete entries within a forEach loop of a javascipt map?" doesn't apply, because you're not doing that. As Thilo points out in a comment, the only real concern would be whether id
was still a valid identifier for what you're deleting and whether deletion is still appropriate (in both cases, presumably it is, but it's worth flagging up).
But answering the question in the title (for future readers), let's change that code a bit so it's not doing the deletion later in an async callback:
sessions.forEach(function(session, id) {
if (shouldDelete(session)) {
sessions.delete(id);
}
});
Live Example:
const sessions = new Map([
["a", {name: "a", value: 1}],
["B", {name: "B", value: 2}],
["C", {name: "C", value: 3}],
["d", {name: "d", value: 4}],
]);
// Delete the ones whose name is in upper case
sessions.forEach((session, id) => {
console.log(`Visiting ${id}`);
if (/^[A-Z]$/.test(session.name)) {
console.log(`*** Deleting ${id}`);
sessions.delete(id);
}
});
console.log("Result:");
console.log(JSON.stringify([...sessions.entries()], null, 4));
// Notice that deleting `B` didn't make us skip `C`
.as-console-wrapper {
max-height: 100% !important;
}
When MDN lets you down, turn to the specification:
Each entry of a map's [[MapData]] is only visited once. New keys added after the call to forEach
begins are visited. A key will be revisited if it is deleted after it has been visited and then re-added before the forEach call completes. Keys that are deleted after the call to forEach
begins and before being visited are not visited unless the key is added again before the forEach
call completes.
(my emphasis)
The emphasized bit makes it clear that deleting during the forEach
isn't a problem.
Since forEach
is also used with arrays, it's worth pointing out that if you "delete" an entry from an array using (say) splice
within a forEach
callback, it's likely to cause a bug. Here's an example similar to the above, but notice that "C" doesn't get removed:
const sessions = [
{name: "a", value: 1},
{name: "B", value: 2},
{name: "C", value: 3},
{name: "d", value: 4},
];
// Delete the ones whose name is in upper case
sessions.forEach((session, index) => {
console.log(`Visiting ${index} (${session.name})`);
if (/^[A-Z]$/.test(session.name)) {
console.log(`*** Deleting ${index} (${session.name})`);
sessions.splice(index, 1);
}
});
console.log("Result:");
console.log(JSON.stringify([...sessions.entries()], null, 4));
// Notice that deleting `B` made us skip `C`!!
.as-console-wrapper {
max-height: 100% !important;
}
If you used slice
, you'd get a different problem:
let sessions = [
{name: "a", value: 1},
{name: "B", value: 2},
{name: "C", value: 3},
{name: "d", value: 4},
];
// Delete the ones whose name is in upper case
sessions.forEach((session, index) => {
console.log(`Visiting ${index} (${session.name})`);
if (/^[A-Z]$/.test(session.name)) {
console.log(`*** Deleting ${index} (${session.name})`);
sessions = [...sessions.slice(0, index), ...sessions.slice(index + 1)];
}
});
console.log("Result:");
console.log(JSON.stringify(sessions, null, 4));
// Note how we *thought* we were deleting `C`, but we deleted `d` instead!
.as-console-wrapper {
max-height: 100% !important;
}
So although deleting in Map
's forEach
is safe, deleting in array's forEach
usually isn't.