2

I could not find any hint on https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/forEach, but basically I am trying to do:

sessions.forEach(function (session, id) {
   someConditionReached(id, function(err, res)) {
      if (shouldDelete(err, res)) {
         sessions.delete(id);
      }
   }
});

Would this cause any trouble? If so, should I just make a temp-array to collect all obsolete sessions and remove them after the forEach? Problem is, that the condition in the loop is async/callback based, so it would be cumbersome to do deferred deletion.

cweiske
  • 30,033
  • 14
  • 133
  • 194
user826955
  • 3,137
  • 2
  • 30
  • 71
  • what is `sessions.delete()`? – Nina Scholz Sep 05 '16 at 09:52
  • 1
    @NinaScholz: It's the method that removes entries from a map; http://www.ecma-international.org/ecma-262/7.0/index.html#sec-map.prototype.delete. – T.J. Crowder Sep 05 '16 at 09:53
  • The only potential trouble I can see is that you later (after the loop, but before the callbacks fire) decide that the session is being extended (and should no longer be deleted). But presumably the `shouldDelete` takes care of that. – Thilo Sep 05 '16 at 10:04

1 Answers1

2

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.

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • 2
    Note that the `forEach` here will complete before any delete calls are issued (because they are all happening in a callback). So nothing is being added or removed during establishment of the keyset that is to be deleted (and anything that happens after the `forEach` does not affect the keyset either). – Thilo Sep 05 '16 at 10:02
  • Thanks for pointing out that the delete actually happens within the callback. While this is true, couldn't it be that in case the callback is in fact _synchronous_ the delete will happen before `forEach` ends? Since I am calling a libary/module function, I can only guess whether the callback will be sync or async. – user826955 Sep 05 '16 at 11:09
  • @user826955: You shouldn't be *guessing* at all. If the library has an API that accepts a callback, and its documentation doesn't tell you clearly whether it will call it synchronously or asynchronously, that's a serious bug in the library documentation and should be reported and treated as such. – T.J. Crowder Sep 05 '16 at 11:10
  • I forgot to mention that I am using nodejs. The library I am talking about is the `express-session` or more precisely the underlying `session-store` (https://github.com/expressjs/session) which can be one of many implementations. Most likely, the callback _should_ be async in most of the cases/backends, but I doubt I can safely tell. – user826955 Sep 05 '16 at 11:14
  • @user826955: If it *might* be async, the only rational design for it is to ensure that it it's *always* async. I'm sure the ExpressJS team have been rational. :-) – T.J. Crowder Sep 05 '16 at 11:18
  • The express team yes, but for my understanding the numerous store-implementations (look at the list at https://github.com/expressjs/session#compatible-session-stores) are implemented by third party authors. – user826955 Sep 05 '16 at 11:22
  • @user826955: I understand, but you're not calling those directly. It would be very silly indeed for the ExpressJS team to leave the API open to that kind of chaos by not ensuring that plugins work correctly. – T.J. Crowder Sep 05 '16 at 11:35
  • 1
    Note that while this might be safe for maps, deleting elements from an array using `splice` as part of a forEach is _not_ safe, so if you're tempted to write `list.forEach( (element, position) = > { ...; if (shouldDelete(element) { list.splice(position,1); }})`, now you've introduced a concurrent modification bug. (commented mostly because this seems to be the top google result when searching for how to delete from an array during a foreach) – Mike 'Pomax' Kamermans Aug 21 '20 at 17:09
  • 1
    @Mike'Pomax'Kamermans - Ah, if this is coming up in those searches, that's an important point to make. I've added it to the answer, thanks! – T.J. Crowder Aug 21 '20 at 17:48