The basket was a Map.
Product id to quantity.
It was the right choice.
Quick lookups,
keys in the order they were added,
size without counting.
To keep it across a refresh,
the page saved it.
localStorage.setItem("basket", JSON.stringify(basket));
And read it back on load.
The unit test checked
that setItem was called.
It was.
Then the support tickets started.
I added three things,
the page reloaded,
and the basket was empty.
Every customer who refreshed
lost everything they had chosen.
Nothing threw.
Nothing logged.
Open the console and try it.
JSON.stringify(new Map([["sku-1", 2]]))
The answer is "{}".
An empty object.
JSON.stringify writes
the own enumerable properties
of an object.
A Map keeps its entries
somewhere else,
in internal storage
that is not a property at all.
So from the outside
a full Map
looks exactly like an empty object.
A Set does the same.
JSON knows six kinds of thing.
Objects, arrays, strings,
numbers, true and false, and null.
Everything else
is translated on the way in,
and the translation is not always kind.
A Date becomes a string,
and comes back as a string.
undefined in an object
is dropped without a word.
NaN becomes null.
A BigInt throws,
which at least
tells you something.
The fix for the Map is short.
Write it as a list of pairs.
JSON.stringify([...basket])
Read it back
by handing the pairs to the constructor.
new Map(JSON.parse(saved))
If every key is a string,
Object.fromEntries works too,
but the pairs keep keys of any type.
The real fix is in the test.
Checking that save was called
proves the code ran.
It says nothing
about what was written.
Test the round trip.
Put a basket in,
save it,
load it,
and compare what comes out
with what went in.
That one test
would have failed
the first time it ran.
Any time data leaves memory,
for storage,
for a queue,
for another service,
it passes through a format
that only knows some of your types.
The question is never
whether it was saved.
The question is
whether it comes back
as the same thing.
– Serguey Asael Shinder
