JavaScript’s Date object turned 30 this year. It was copied from Java’s java.util.Date in 1995, and Java deprecated most of its own Date API years later. The JavaScript community has been working around it ever since, producing a succession of date libraries — Moment.js, then Luxon, then date-fns and Day.js — each trying to paper over the same fundamental design problems. Bloomberg’s account of the Temporal proposal documents nine years of work getting a real fix into the language. The TC39 Temporal proposal advanced to Stage 3 in March 2021 and remains one of the longest-running Stage 3 proposals in TC39 history. That timeline reflects what correctness about time actually requires.
What Date Gets Wrong
The surface complaints are well-known: months are 0-indexed (January is 0, December is 11, but day-of-month starts at 1), the object is mutable, Date.parse() behaves inconsistently across engines for the same input string. These are irritating but fixable in a wrapper. The structural problem is deeper.
Date stores a UTC millisecond count and presents it through the local system time zone for display. This conflation means the object tries to represent at least four conceptually distinct things simultaneously: a precise moment in time, a calendar date, a wall-clock reading, and a local offset. None of the four is modeled correctly because they all collapse to the same underlying integer.
Date.parse("2020-01-01") is supposed to return UTC midnight per ISO 8601, but historically some engines parsed it as local midnight. Adding one month to March 31 via .setMonth(3) silently produces May 1, because April 31 does not exist and Date just overflows without a word about it. DST is where the problems become production bugs: 2025-03-09T02:30:00 does not exist in America/New_York because clocks spring forward from 2:00 to 3:00. Date adjusts silently.
There is no duration type. Subtracting two Date values gives milliseconds. Computing “three months from now” requires writing arithmetic the object provides no tools for, and the common approximation of treating a month as 30 days is always producing subtly wrong answers somewhere downstream.
Every library that came along sat on top of the same broken foundation. Moment.js gave you immutable-ish operations and a duration type, but it wrapped Date and carried its timezone limitations until the separate moment-timezone plugin shipped a bundled copy of the IANA timezone database. Luxon was designed to use the browser’s Intl.DateTimeFormat API for timezone data instead of bundling it, which was a better architecture, but it still presented one primary type for everything. Notably, the authors of Moment.js, including Maggie Pint, became TC39 Temporal champions. They built the library, understood its ceilings, and concluded the right answer required a language-level fix.
The Type Taxonomy
Temporal introduces a set of types organized around two questions: does this value include a time of day, and does it include a time zone? The answers determine which type you should reach for.
Temporal.Instant is a precise point in time, UTC-only, stored as a BigInt of nanoseconds since the Unix epoch. No calendar opinion, no timezone conversion, just a position on the timeline. This is what you want for event logs, database records, and comparing timestamps from different locations.
Temporal.PlainDate is a calendar date without time-of-day or time zone: year, month, day. Birthdays, fiscal quarters, holidays, deadlines. Things where “what time zone” is a meaningless question.
Temporal.ZonedDateTime is the full combination: date, time, time zone, and calendar system. This is what you use when you need to schedule something with DST-correct arithmetic.
// 1:30 AM the day before DST spring-forward in New York
const before = Temporal.ZonedDateTime.from(
'2025-03-09T01:30:00[America/New_York]'
);
// Add 24 hours vs. add 1 day — these are different operations
const plus24h = before.add({ hours: 24 });
// 2025-03-10T02:30:00-04:00[America/New_York]
const plusDay = before.add({ days: 1 });
// 2025-03-10T01:30:00-04:00[America/New_York]
The distinction between “24 hours” and “1 calendar day” across a DST boundary is something Date cannot express at all, because there is no mechanism to say which you mean. ZonedDateTime forces you to choose by giving you both options. Months are 1-indexed throughout. Temporal.PlainDate.from('2025-03-11').month returns 3. This alone eliminates an entire class of off-by-one errors.
Temporal.PlainDateTime handles date plus time with no time zone, which sounds oddly specific until you need it. Recurring meetings specified before you know where they will happen, business logic that should not shift with DST, template dates for user input — these all want a wall-clock reading without timezone attachment.
Explicit Overflow Handling
One design pattern that runs throughout the proposal is how it handles invalid results. Adding one month to January 31 produces a date that does not exist. Rather than picking a silent default, Temporal requires you to declare your intent:
const jan31 = Temporal.PlainDate.from('2025-01-31');
// Constrain: clamp to the last valid day
jan31.add({ months: 1 }, { overflow: 'constrain' }); // 2025-02-28
// Reject: throw if the result would be invalid
jan31.add({ months: 1 }, { overflow: 'reject' }); // RangeError
The same pattern applies to DST gaps. If you construct a ZonedDateTime during the hour that was skipped by a spring-forward, you can pass disambiguation: 'earlier', 'later', 'compatible', or 'reject'. The 'compatible' option matches Date’s behavior for backwards compatibility when you need it. Every place where Date silently picked a result, Temporal either provides a sensible explicit default or surfaces the ambiguity as a required choice.
Durations as First-Class Values
Temporal.Duration stores all of its components separately. “1 month and 15 days” is not converted to a day count or a millisecond integer — it remains { months: 1, days: 15 }. This matters because month lengths vary, and converting prematurely produces wrong answers:
const d = Temporal.Duration.from({ months: 1, days: 15 });
// Not flattened — still 1 month and 15 days
const start = Temporal.PlainDate.from('2025-01-31');
const end = Temporal.PlainDate.from('2025-03-01');
const dur = start.until(end, { largestUnit: 'months' });
// P1M1D — 1 month and 1 day, counting forward from Jan 31
Calendar-aware duration arithmetic requires a reference point, because “1 month from March 31” has a different answer than “1 month from March 1.” Temporal makes this explicit through the relativeTo parameter. New users frequently hit RangeError: a starting point is required for this operation and find it jarring, but the error is correct. The question has no single right answer without a starting point, and every library that silently treated months as 30 days was producing subtly wrong results in production.
Bloomberg’s Role and the Scale of the Work
Bloomberg was the primary corporate sponsor of Temporal from around 2018 onward. The company funded Igalia, a web standards consultancy, to write the formal specification text and the reference polyfill. Philip Chimento and Ujjwal Sharma at Igalia did the bulk of that work under Bloomberg’s backing. Bloomberg’s motivation was direct: financial applications run constant date arithmetic on time series data, with operations like “9:30 AM New York time, every trading day, through DST transitions” being both performance-critical and correctness-critical. The flaws in Date were causing real bugs in Bloomberg Terminal and related products.
The resulting spec is approximately 200 pages of formal ECMA-262 language, one of the largest single additions to JavaScript. For comparison, the entire ES2015 spec was around 600 pages. Temporal includes 20+ interacting types, support for roughly 20 calendar systems (including Hebrew, Islamic, Persian, Japanese, and Chinese calendars via CLDR data), nanosecond precision, custom calendar and timezone extension mechanisms, and DST edge-case handling at every operation level.
For integration with the existing ecosystem, Temporal.ZonedDateTime works directly with Intl.DateTimeFormat:
const zdt = Temporal.ZonedDateTime.from(
'2025-03-11T09:30:00-04:00[America/New_York]'
);
zdt.toLocaleString('en-US', { dateStyle: 'full', timeStyle: 'short' });
// "Tuesday, March 11, 2025 at 9:30 AM"
// Converting from legacy Date
const legacy = new Date();
const instant = Temporal.Instant.fromEpochMilliseconds(legacy.getTime());
const zdt2 = instant.toZonedDateTimeISO('America/New_York');
Browser Support and the Polyfill Path
V8 ships a partial Temporal implementation behind --harmony-temporal in Node.js 20+. SpiderMonkey has work in progress. No major browser ships it unflagged as of early 2025. The @js-temporal/polyfill package, the official reference polyfill that passes the TC39 test262 suite, is the practical path today:
npm install @js-temporal/polyfill
import { Temporal } from '@js-temporal/polyfill';
The polyfill runs to around 380KB minified because it bundles the timezone database and calendar computation tables. That cost disappears once engines ship native support. For Stage 4, Temporal needs two complete, independent implementations in shipping browsers with all test262 tests passing. Based on current engine progress, that is most likely a 2025 or 2026 outcome.
What Happens to the Libraries
Moment.js is in maintenance mode, and its documentation explicitly recommends Temporal as the future path. This is not a coincidence; Maggie Pint and Matt Johnson-Pint were Moment authors who became TC39 champions because they understood exactly what the library could not fix from userland. Luxon covers many of the same concerns and handles DST correctly through Intl.DateTimeFormat, but it remains a userland dependency with all the tradeoffs that implies. date-fns is a solid functional wrapper around Date but inherits all of Date’s conceptual limitations.
Once Temporal ships natively, the case for reaching for any date library in new code largely disappears. The type system covers nanosecond timestamps, multi-calendar date arithmetic, DST-correct scheduled events, duration arithmetic, and explicit overflow handling. The polyfill works in production today. The wait is for the performance and bundle-size benefits of native engine implementations, and for the formal Stage 4 designation.
Nine years of specification work is a long time, but the alternative — shipping something smaller and faster that required breaking changes later — would have cost more. The spec size is the honest accounting of what correctness about time actually requires.