Why 90 outranks 100
What you saw
Six players, all loaded, all with correct scores — ranked into an order that makes no numeric sense. Curl the endpoint and you get this:
Ben 90
Cleo 9
Eve 8
Dan 25
Finn 110
Ada 100
The leader scored 90. The two highest scores in the game, 110 and 100, are dead last. It isn't reversed (a reverse would put 8 on top). It isn't random (refresh and it's identical). It's consistently, structurally wrong.
What's actually happening
Two things have to line up for this bug, and both are easy to miss on their own.
First, the scores are strings. Look at db.js:
{ id: 'p-finn', name: 'Finn', score: '110' },
{ id: 'p-ada', name: 'Ada', score: '100' },
{ id: 'p-ben', name: 'Ben', score: '90' },
Those quotes are the whole story. The scores arrived over JSON from game clients and nobody ever turned them into numbers. '110' is text, not the quantity one hundred and ten.
Second, the comparator compares them with >. Here's the sort in server.js:
const ranked = players.sort((a, b) => {
if (a.score > b.score) return -1
if (a.score < b.score) return 1
return 0
})
When both operands are strings, > does not compare magnitude. It compares lexicographically — character by character, like words in a dictionary. So it asks "does '9' come after '1'?" — yes — and decides '90' > '100'. The first character settles it; '90' never even gets to its second digit. That's how a 90 beats a 100, and how every three-digit score loses to a two-digit one that happens to start with a bigger first digit.
The reason it's sneaky: with single-digit scores it would have looked perfect. '8' < '9' lexicographically and numerically agree. The bug only surfaces once your numbers have different lengths — exactly when real data finally shows up.
Note that subtraction would have hidden the bug too: a.score - b.score coerces both strings to numbers, so '90' - '100' is -10 and sorts correctly. It's specifically the relational operators (>, <) on strings that betray you, because they have a perfectly valid string meaning and never complain.
The fix
You can fix this in two places, and the better answer depends on how much you trust your data.
Fix at the comparison — coerce inside the comparator:
const ranked = players.sort((a, b) => Number(b.score) - Number(a.score))
Number('110') is 110, subtraction returns a real number, and descending order falls out of b - a. This is the minimal change and it makes the sort correct regardless of how the data was stored.
Fix at the source — turn scores into numbers the moment they enter the system, in db.js:
async function getPlayers() {
await delay(120)
return PLAYERS.map((p) => ({ ...p, score: Number(p.score) }))
}
Now score is a real number everywhere downstream, and a.score > b.score in the original comparator suddenly does the right thing — because now it's comparing numbers, not text. This is usually the stronger fix: it stops every future consumer of this data from being fooled, not just this one sort.
Either way, the leaderboard reads 110, 100, 90, 25, 9, 8 — and stays correct when you drop in a 5 or a 999.
The bug class: string-shaped numbers and lexicographic comparison
This is the "numbers that are secretly strings" bug, and its favorite hiding spot is sorting. The root cause is a type that never got checked at the boundary: data came in from JSON, a form field, a CSV, a query param, localStorage, an environment variable — all of which hand you strings — and it flowed deep into the app still wearing quotes.
It's rampant in AI-generated code for two reasons. First, generated sample data and generated parsers both love to quote numbers ("score": "110"), so the types are wrong before any logic runs. Second, > and < on strings are valid — there's no error, no warning, no crash — so nothing ever points at the mistake. The code looks completely reasonable and even passes a quick test with small numbers.
How to recognize it in the wild:
- A sort is "almost right" but a few items are wildly out of place — and the misplaced ones tend to be the longest or shortest numbers (
100sorting near10, or2sorting after19). "9" > "100"istrue,"19" < "2"istrue,"10" < "9"istrue— if any of those make you wince, look for unquoted-looking numbers that are actually quoted.- Dates as strings sort correctly only in
YYYY-MM-DDform, and break the instant the format varies — same root cause.
The one-line mental model: > and < on strings sort like a dictionary, not a number line. Any time you compare or sort values that came in from outside the program, confirm their type first — coerce numbers with Number(...) at the boundary, and your comparators stop lying to you. The fastest tell: if a comparator uses >/< instead of subtraction and the values might be strings, you've probably found one.