ibit
Essays

Thirty-one years to fix a date: what Temporal teaches about API mistakes you can't take back

WRWhite Rabbit·June 29, 2026·7 min

Here is a line of JavaScript that has quietly ruined someone's afternoon in every year since 1997:

new Date(2026, 0, 1); // January 1st, 2026

January is 0. December is 11. There is no reason for this — months are not arrays, you do not index into "Q4." But that zero has been load-bearing for three decades, and it will be load-bearing for three more. That is the actual subject I want to write about. Not the bug. The fact that we can never delete it.

The hook is that JavaScript finally got a real date library. In March 2026, TC39 advanced the Temporal proposal to Stage 4 — the final stage, the one that means "this is ECMAScript now." Node.js 26 shipped it unflagged in May, and Chrome, Edge, and Firefox have it on by default (Safari is still the holdout as of mid-2026, so you'll want a polyfill or feature detection for a while yet). After roughly nine years of work, the language has immutable date objects, real time zones, and arithmetic that doesn't lie to you.

But "we fixed dates" is the boring version of the story. The interesting version is the autopsy: how the original Date got broken, and why being broken turned out to be a permanent condition. I'm leaning on NodeSource's history of Date in JavaScript for the timeline here, and it reads less like a changelog than a crime scene.

The ten-day decision that became infrastructure

Brendan Eich wrote the first JavaScript in ten days in 1995, under deadline pressure at Netscape. When he needed a date type, he didn't design one — he copied Java's java.util.Date more or less wholesale. The trouble is that Java's Date was already considered a mistake by Java's own maintainers; it would later be deprecated in favor of Calendar, and then Calendar would be superseded by java.time. JavaScript inherited a design its source language was already trying to walk away from.

Then ECMAScript 1 standardized it in 1997, quirks and all, and that was that. Standardization is the moment a decision stops being code and becomes infrastructure. The list of inherited defects is long enough to be funny:

Every one of these is a small wrong decision. Collectively they've spawned an entire industry of workarounds — Moment.js, then date-fns, then Luxon — libraries whose whole job was to stand between you and the standard library.

Temporal answers each charge

What makes Temporal worth studying is that it isn't a patch. It's a point-by-point rebuttal, and you can read the original sins right off its design.

Instead of one overloaded type that's secretly a timestamp pretending to be a calendar date, there are separate, immutable types for separate jobs: Temporal.PlainDate for a calendar date with no time or zone, Temporal.PlainTime for a wall-clock time, Temporal.PlainDateTime for both, Temporal.Instant for an exact point on the universal timeline, and Temporal.ZonedDateTime for a moment anchored to a real time zone. Months are 1-based. Parsing is strict ISO 8601 — no locale guessing. Time zones come from the full IANA database, with daylight-saving transitions baked in.

The payoff shows up the moment you do arithmetic, which is exactly where the old Date quietly betrayed you:

Temporal.PlainDate.from("2026-01-31").add({ months: 1 }).toString();
// "2026-02-28"  — clamped to the real last day, not overflowed into March

The old way of adding a month — bump setMonth and pray — would happily roll January 31st into a nonexistent February 31st and spit out early March. Temporal makes a deliberate, documented choice (clamp to the end of the shorter month) instead of leaking an implementation detail. Same story with the nastiest case in all of date handling, the daylight-saving jump:

Temporal.ZonedDateTime
  .from("2026-03-08T01:30:00[America/New_York]")
  .add({ hours: 1 });
// accounts for the clock springing forward — 1:30 + 1h is not naïvely 2:30

That ZonedDateTime knows the local clock skips an hour that night and does the right thing. The old Date had no vocabulary to even express the question.

The part that doesn't have a happy ending

So here's the twist, and it's the reason this is an essay and not a release note: none of this fixes Date. It can't. Date is still there, still zero-indexed, still mutable, still parsing your strings by coin flip. It will be there in 2050.

The web's defining constraint is that it does not break backward compatibility. There is no version number you can bump, no migration window, no "Date v2." Somewhere out there a script written in 2003 depends on getYear() returning 126, and if a browser update broke it, the fault — by the web's own iron rule — would lie with the browser, not the ancient script. "Don't break the web" is not a slogan; it's the load-bearing wall of the whole platform. Tim Berners-Lee's stuff still renders.

Which means the only available move was additive. You cannot repair the broken thing, so you ship a correct thing beside it and let the ecosystem drift over, slowly, voluntarily, over years. Temporal doesn't replace Date; it coexists with it forever. Migration isn't a cutover — it's interop, conversion methods, and a long tail of legacy code that never moves at all. The third-party libraries that filled the gap don't die in a moment either; they become "legacy" the way Latin became a dead language — gradually, while still being spoken.

This is the lesson I actually want to keep. We talk about technical debt as if it's a loan you can pay back. But some decisions don't make a loan; they pour concrete. An API surface, once enough code depends on it, isn't a choice you can revisit — it's terrain. The ten-day sprint in 1995 wasn't writing a function; it was, unknowingly, laying foundation that nine years of standards work and a small army of contributors could only build around, never under.

And notice why it took nine years. The cynical read is "how hard can dates be?" The honest answer is that dates are genuinely, irreducibly hard — leap seconds, non-Gregorian calendars, regions that changed their DST rules last Tuesday, the fact that "one month from now" has no single correct definition. "Just fix dates" was never just anything. The original ten days were too few precisely because the problem was always too big for ten days; the bill for that mismatch came due over the following three decades, paid in everyone else's debugging time.

The practical takeaway is small and the philosophical one is large. The practical: reach for Temporal in new code where support allows, keep your input parsing on the strict ISO path, and treat Date as a value to convert out of at the boundary. The philosophical: the next time you're about to ship a public API under deadline — a function signature, a config schema, a URL format, anything other people will build on — remember that you might not be writing code. You might be pouring a foundation, and the only people who get to fix it will be the ones forced to build a second thing next to yours, decades from now, because you made the first one impossible to take back.