[{"data":1,"prerenderedAt":6},["ShallowReactive",2],{"post-content-javascript-temporal-replace-date":3},{"content":4,"lastModified":5},"\u003Cp>The clocks changed last weekend and, right on schedule, a reminder in an app I maintain fired an hour early for some of our users. I opened the file responsible, which in every JavaScript codebase I have ever worked on is called \u003Ccode>dateUtils.ts\u003C\u002Fcode>, and found the usual pile of workarounds: a hand-rolled \u003Ccode>addMonths\u003C\u002Fcode>, a comment explaining why someone subtracted 1 from a month, and a helper that exists only because \u003Ccode>Date\u003C\u002Fcode> cannot say \"9am in Tokyo\".\u003C\u002Fp>\n\n\u003Cp>Here is the thing though. The reason that file exists just went away. Temporal reached Stage 4 at TC39's March 2026 meeting, so the API is finished, and it is already running by default in Firefox, Chrome, Edge, and Node.js 26. I have been moving small pieces of real projects onto it for a few weeks. The honest version follows.\u003C\u002Fp>\n\n\u003Cfigure>\n  \u003Cimg src=\"https:\u002F\u002Fimages.pexels.com\u002Fphotos\u002F5833837\u002Fpexels-photo-5833837.jpeg?auto=compress&cs=tinysrgb&w=1200\" alt=\"Three desk clocks set to different time zones next to a tablet and a notebook\" loading=\"lazy\" \u002F>\n\u003C\u002Ffigure>\n\n\u003Ch2>Why Date kept breaking things\u003C\u002Fh2>\n\n\u003Cp>The classic one, which I have shipped twice in my career:\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode class=\"language-javascript\">const d = new Date(2026, 0, 31)  \u002F\u002F January 31\nd.setMonth(1)                     \u002F\u002F \"February 31\" overflows to March 3\n\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>Months are zero-indexed, the object is mutable, parsing is inconsistent across engines, and expressing \"an event at 9am in a specific place\" means dragging in a library. That last point is why date-fns and Luxon exist, and why every developer has a bug story that only happens twice a year. Mine fired this Sunday.\u003C\u002Fp>\n\n\u003Cp>Temporal fixes all of it, not with a better \u003Ccode>Date\u003C\u002Fcode>, but by not trying to be one type that does everything. It gives you separate types for separate concepts, and picking the right one does most of the work.\u003C\u002Fp>\n\n\u003Ch2>One type per concept\u003C\u002Fh2>\n\n\u003Cp>\u003Ccode>Instant\u003C\u002Fcode> is an exact moment on the timeline. \u003Ccode>PlainDate\u003C\u002Fcode> is a calendar date, no time attached. \u003Ccode>PlainDateTime\u003C\u002Fcode> is wall-clock date and time with no zone. \u003Ccode>ZonedDateTime\u003C\u002Fcode> is an instant plus a named IANA zone, and that is the one for anything that corresponds to a real event. Every value is immutable, so a utility function can no longer reach into your date and quietly change it.\u003C\u002Fp>\n\n\u003Cp>Arithmetic behaves the way you meant it to. Adding a month to January 31 clamps to February 28 instead of spilling into March, and \u003Ccode>overflow: \"reject\"\u003C\u002Fcode> turns the clamp into an error:\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode class=\"language-javascript\">const due = Temporal.PlainDate.from(\"2026-01-31\");\n\ndue.add({ months: 1 }).toString();                        \u002F\u002F \"2026-02-28\"\ndue.add({ months: 1, overflow: \"reject\" });               \u002F\u002F throws\ndue.until(Temporal.PlainDate.from(\"2026-11-06\")).days;    \u002F\u002F 279\n\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>Months are one-based now. That sounds like a footnote, but I spent over a decade mentally subtracting one from every \u003Ccode>getMonth()\u003C\u002Fcode> call, so it took a while to stop.\u003C\u002Fp>\n\n\u003Ch2>DST is where it earns its keep\u003C\u002Fh2>\n\n\u003Cp>This is the example I keep pasting into code review. It is the beginning of March in New York, right before the spring-forward night:\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode class=\"language-javascript\">const alarm = Temporal.ZonedDateTime.from(\"2026-03-07T09:00:00[America\u002FNew_York]\");\n\nalarm.add({ days: 1 }).toString();   \u002F\u002F 2026-03-08T09:00:00-04:00, still 9am\nalarm.add({ hours: 24 }).toString(); \u002F\u002F 2026-03-08T10:00:00-04:00, 10am\n\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>Adding one calendar day keeps the wall-clock time through the transition. Adding 24 hours does not. Plain \u003Ccode>Date\u003C\u002Fcode> cannot even express that distinction, and it is the difference between a reminder firing at 9am and one firing at 10am after the clocks change.\u003C\u002Fp>\n\n\u003Cp>Which is to say, the bug I was looking at on Sunday.\u003C\u002Fp>\n\n\u003Ch2>Using it today\u003C\u002Fh2>\n\n\u003Cp>Support right now: Firefox 139 and later (there since May 2025), Chrome and Edge 144 (January 2026), and Node.js 26 (May 2026, no flag). Deno has it too. Safari is the holdout: the API lives only in Safari Technology Preview so far. WebKit says default-on support is coming later this year, but I stopped betting deadlines on browser timelines.\u003C\u002Fp>\n\n\u003Cp>On the server it is just there if you run Node 26. In the browser, feature-detect and polyfill:\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode class=\"language-javascript\">if (typeof Temporal === \"undefined\") {\n  const { Temporal } = await import(\"temporal-polyfill\");\n  globalThis.Temporal = Temporal;\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>There are two polyfills. \u003Ccode>temporal-polyfill\u003C\u002Fcode> from the FullCalendar team is the smaller one with stable releases, and \u003Ccode>@js-temporal\u002Fpolyfill\u003C\u002Fcode> is the reference implementation, closer to the spec but heavier. I picked the small one for client bundles. Polyfilling a finished standard has one nice property: when Safari finally ships, you delete the import and nothing else changes.\u003C\u002Fp>\n\n\u003Ch2>Migrating without a rewrite\u003C\u002Fh2>\n\n\u003Cp>I did not convert the whole codebase, and you should not either. date-fns still works and Luxon is not leaving our backend this quarter. The pattern that has held up is converting at the boundary: when a date enters from an API or a database row, turn it into a Temporal type immediately, do all the reasoning there, and convert back only where an old API insists:\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode class=\"language-javascript\">\u002F\u002F legacy Date in, Temporal for the logic\nconst instant = new Date(row.created_at).toTemporalInstant();\nconst local = instant.toZonedDateTimeISO(\"Asia\u002FShanghai\");\n\n\u002F\u002F and back out, when some old function demands a Date\nconst legacy = new Date(instant.epochMilliseconds);\n\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>Formatting needs nothing new either. \u003Ccode>Intl.DateTimeFormat\u003C\u002Fcode> accepts Temporal objects directly, so the display side keeps working while the logic side improves.\u003C\u002Fp>\n\n\u003Ch2>Small things worth knowing\u003C\u002Fh2>\n\n\u003Cp>\u003Ccode>valueOf\u003C\u002Fcode> throws on purpose, so sloppy coercion and \u003Ccode>==\u003C\u002Fcode> comparisons fail loudly instead of silently comparing garbage. Use \u003Ccode>equals()\u003C\u002Fcode> for equality and \u003Ccode>Temporal.PlainDate.compare\u003C\u002Fcode>, which doubles as a sort function. Those two choices felt annoying for a day and then felt correct.\u003C\u002Fp>\n\n\u003Cp>And one thing I got wrong in my first week: a \u003Ccode>Duration\u003C\u002Fcode> does not rebalance itself. \u003Ccode>a.until(b)\u003C\u002Fcode> hands you a raw duration, and you call \u003Ccode>round()\u003C\u002Fcode> or \u003Ccode>total()\u003C\u002Fcode> to get what you actually meant. I assumed it would normalize and shipped a log line that said \"hours: 297\". Nothing broke, but the monitoring graph looked very dramatic.\u003C\u002Fp>\n\n\u003Cp>I am not going to pretend this deletes every date library by Christmas. Safari has not shipped and the polyfill costs real kilobytes. But new code I write is Temporal, and for the first time in a long JavaScript career I can picture \u003Ccode>dateUtils.ts\u003C\u002Fcode> not existing. The platform has been taking its jobs back, the same way \u003Ca href=\"\u002Fposts\u002Freplaced-tailwind-native-css\u002F\">native CSS did\u003C\u002Fa>.\u003C\u002Fp>\n\n\u003Cp>My migration notes and that boundary helper live in \u003Ca href=\"\u002Fsnippetark\u002F\">Snippet Ark\u003C\u002Fa>, next to the config snippets I collected when I \u003Ca href=\"\u002Fposts\u002Fnode-26-lts-upgrade-breaking-changes\u002F\">upgraded to Node 26\u003C\u002Fa> and found Temporal already waiting. If you keep a list of things the platform finally fixed, this one goes near the top.\u003C\u002Fp>\n","2026-11-06",1793940102307]