A production schedule sorted correctly for years. Then it didn’t, and the trail led to March 4, 1973, a date no game in the system had ever come near. The bug was real, the dates on screen were modern, and the root cause was a number that turned into a string at exactly the wrong moment.

Here’s the setup. The production app renders team schedules in the browser by building a list of schedule nodes and sorting them by a priority field. For a game node, that priority starts life as a plain number: Number(gamedate), where gamedate is a JavaScript getTime() value, milliseconds since the Unix epoch. Sort by that number and games come out in chronological order. Simple, correct, and it had been correct since the code was written.

The trap was a tie-breaker. When two nodes needed a secondary sort key, a location string got concatenated onto the priority so games at the same time would order consistently by venue. Concatenating a string onto a number does what you’d expect in JavaScript: the result is a string. So now the same list held two kinds of priority values. Most nodes carried a numeric priority. The ones that picked up a location tie-breaker carried a string priority that started with the same digits but was no longer a number.

That mix is the whole bug. The sort comparator compared those priorities. When both sides were numbers it subtracted them and got numeric order. The moment one side stopped being a number, the comparison fell into a different branch and compared the raw strings instead, and string comparison is lexicographic. Lexicographic ordering does not care about magnitude. It compares character by character, left to right, and stops at the first position that differs. That is what inverts the list. A 12-digit timestamp like "123456789012" and an 11-digit one like "99999999999" differ at the very first character, 1 versus 9. Since 1 is less than 9, the 12-digit string sorts before the 11-digit string, even though as numbers the 12-digit value is far larger. A newer game with a longer timestamp jumps ahead of an older game with a shorter one.

Why March 4, 1973? Because that is where getTime() crosses a digit-count boundary. Milliseconds since the epoch hit eleven digits, then keep climbing until they tick over to twelve. That rollover from an 11-digit to a 12-digit millisecond value happens right around March 4, 1973. Every modern timestamp is twelve or thirteen digits. The bug had nothing to do with games actually scheduled in 1973. The 1973 boundary is just the power-of-ten line where the string length of the timestamp changes, and length is what lexicographic comparison silently keys on once your numbers have become strings of different widths.

The conditions to trigger it were narrow. You needed a schedule whose timestamps straddled a digit-width boundary, and you needed some of those games to carry a location tie-breaker that stringified the priority. Hit both and the comparator started mixing numeric and lexical ordering in the same sort, and the list scrambled. Miss either and everything looked fine, which is exactly why this survived for years. No test fixture reaches for a date on the far side of a 1973 epoch boundary, and no casual test pairs that with the location tie-break path.

The fix lived in the sort-priority assignment code, and it is a one-liner in spirit: make the timestamp a fixed width before anything concatenates onto it. Zero-pad the numeric priority to a fixed 15-character string so that lexical comparison and numeric comparison produce the same order. The shape of it:

padStart(String(Number(priority) || 0), 15)

Fifteen characters is comfortably wider than any getTime() value you’ll see for centuries, so every timestamp becomes the same length. Once they’re all the same length, leading characters line up by place value, and string comparison and number comparison agree. The padding is skipped for time-label nodes, which aren’t game timestamps and don’t want it. One line, placed where the number first becomes a key.

The fix sits right next to a pre-existing hack for pre-1970 negative timestamps, labeled in the source as a fix for an old ticket. This exact code path already had history with epoch edge cases. Someone had previously been bitten by timestamps going negative below the epoch, patched it, and moved on, and the digit-width problem was a second, independent landmine in the same neighborhood. Date math attracts these. The epoch is a number line with a sign change at one end and a digit-count change at every power of ten, and a string sort steps on every one of those landmines that a numeric sort steps over.