· 6 min read ·

The Last Major Language to Fix Its Dates: Temporal in Context

Source: hackernews

JavaScript’s Date object was copied from Java’s java.util.Date in 1995, under time pressure, by one engineer, in ten days. Java deprecated most of java.util.Date’s methods in 1997. JavaScript kept it for another three decades.

The Bloomberg Engineering retrospective on Temporal frames the proposal as a nine-year journey. That framing is accurate but narrow. The actual journey is closer to thirty years, and most of it was spent watching every other major programming language solve this problem first.

How Every Other Language Got Here

Python introduced the datetime module in 2001 with a distinction that would define the field: aware versus naive. A naive datetime carries no timezone information. An aware datetime carries a tzinfo. The type forces you to decide, at construction time, whether this object knows what timezone it belongs to. Operations between aware and naive datetime objects raise TypeError.

from datetime import datetime, timezone

# Naive — no timezone
d = datetime(2026, 3, 11, 9, 0)

# Aware — UTC
d_utc = datetime(2026, 3, 11, 9, 0, tzinfo=timezone.utc)

# This raises TypeError
d + d_utc

Python’s solution is not perfect. The zoneinfo module with full IANA database support did not arrive until Python 3.9 in 2020, twenty-three years after the problem was first noted. datetime.date (date-only) and datetime.datetime (date plus time) cover the timezone-naive case for pure calendar dates. The type hierarchy is smaller than Temporal’s but structurally similar.

Java moved faster on the second try. Stephen Colebourne released Joda-Time in 2002, providing LocalDate, LocalDateTime, ZonedDateTime, and DateTime as distinct types. Java officially absorbed this design into the language as java.time in Java 8 (2014), via JSR-310. The gap between Java’s original broken Date and the corrected java.time was seventeen years. JavaScript’s gap is thirty.

C# got Noda Time in 2009, also from Stephen Colebourne. Rust’s chrono crate has distinguished NaiveDate, NaiveDateTime, and timezone-aware DateTime since 2014. Go’s standard time package, while imperfect in other ways, at least always carried timezone information explicitly.

In each case, the same insight drove the solution: a single type that tries to represent both calendar dates and absolute moments in time produces a leaky abstraction. The conversions between the two concepts are lossy and context-dependent. Putting them in one type forces users to track the distinction in their heads rather than in the type system.

JavaScript was the last major general-purpose language without a standard answer.

What Temporal Borrowed and What It Added

The TC39 Temporal proposal reached Stage 3 in March 2021. The champions — including Philipp Dunkel and Jason Williams from Bloomberg, and Philip Chimento and Ujjwal Sharma from Igalia — explicitly studied Noda Time, Joda-Time, and Python’s datetime during design. The resulting type set covers more cases than any of them.

Temporal.Instant is an absolute point in time with nanosecond precision, stored as a BigInt. No calendar, no timezone, just a position on the UTC timeline.

Temporal.PlainDate is a calendar date with no time-of-day and no timezone. Birthdays, holidays, fiscal quarter boundaries. The month property is 1-indexed: March is 3.

Temporal.ZonedDateTime is the full combination: date, time, IANA timezone identifier, and calendar system. This is what you use when you need to schedule something and need the arithmetic to respect DST.

const meeting = Temporal.ZonedDateTime.from({
  year: 2026, month: 3, day: 8,
  hour: 9, minute: 0,
  timeZone: 'America/New_York'
});

// Adding 1 calendar day preserves 9:00 AM even across DST
const nextDay = meeting.add({ days: 1 });
nextDay.hour; // 9

// Adding 24 hours gives you a different absolute moment
const plus24 = meeting.add({ hours: 24 });
plus24.hour; // 10, because DST moved clocks forward that night

The distinction between add({ days: 1 }) and add({ hours: 24 }) is something Date cannot express. Both compile to the same operation in JavaScript today, which is why scheduling code has to include timezone-database lookups by hand.

Beyond these three, Temporal includes PlainDateTime (date plus time, no timezone), PlainTime (time of day, no date), PlainYearMonth (billing cycles, monthly reports), PlainMonthDay (recurring annual events), and Duration (spans of time that preserve month boundaries as month boundaries, not as 30-day approximations).

Why Overflow Handling Matters More Than It Looks

One design decision that runs throughout Temporal is explicit handling of invalid results. Adding one month to January 31 produces February 31, which does not exist. Every library has to pick a behavior. Most pick silently.

const jan31 = Temporal.PlainDate.from('2026-01-31');

jan31.add({ months: 1 }, { overflow: 'constrain' }); // 2026-02-28
jan31.add({ months: 1 }, { overflow: 'reject' });    // RangeError

The same applies to DST gaps. When clocks spring forward and 2:30 AM does not exist, ZonedDateTime requires a disambiguation option rather than silently picking a time:

Temporal.PlainDateTime.from('2026-03-08T02:30:00')
  .toZonedDateTime('America/New_York', { disambiguation: 'earlier' });
  // Use 'earlier', 'later', 'compatible', or 'reject'

This pattern forces you to document your intent in code, which matters in financial systems where the difference between 'earlier' and 'later' during a DST fold can represent a meaningfully different transaction time.

Bloomberg’s Specific Problem

Financial systems do not operate in abstract time. NYSE opens at 9:30 AM Eastern, through DST transitions, on specific trading days that exclude US federal holidays. The London Stock Exchange operates on a different calendar. Tokyo and Hong Kong operate on others. Settlement dates in fixed income often fall on the third business day after a trade, where “business day” is defined per exchange with its own holiday calendar.

None of this is modelable with Date. You can store a UTC timestamp and attach metadata, but the type gives you no help. Bloomberg’s codebase on the JVM used java.time for years. The JavaScript frontend and API layers used Date, and the mismatch was a persistent source of conversion bugs.

The financial context drove specific Temporal decisions. Calendar system support is first-class, not an afterthought: Temporal ships with built-in support for hebrew, islamic, persian, japanese, chinese, buddhist, and roughly a dozen others, all via CLDR data. Market systems that work with Israeli or Saudi counterparties need correct Hebrew and Islamic calendar arithmetic for settlement. Libraries could provide this, but each library provides it differently, and interoperability across library boundaries requires manual conversion.

Bloomberg did not just use the proposal. They funded Igalia to write the specification text and the reference polyfill. That funding relationship is why Stage 3 was reachable: the formal spec is approximately 200 pages, and the polyfill implements enough edge cases that it passes the TC39 test262 suite. Volunteer effort alone was insufficient for a specification of this scope.

The State of Things Now

The @js-temporal/polyfill package is production-usable today.

npm install @js-temporal/polyfill
import { Temporal } from '@js-temporal/polyfill';

const today = Temporal.Now.plainDateISO();
const inThreeMonths = today.add({ months: 3 });

V8 ships partial Temporal support behind --harmony-temporal. SpiderMonkey has implementation work underway. Neither is unflagged in a shipping browser yet. Stage 4 requires two complete implementations passing all test262 tests, which puts full native support somewhere in 2025 or 2026 depending on engine progress.

Moment.js’s official documentation recommends migrating to Temporal when available. Maggie Pint, a former Moment author, was a TC39 Temporal champion. The library authors reached the same conclusion as the language committee: there is no version of a Date wrapper that fully solves the problem from userland.

What the Timeline Reveals

Thirty years is a long time to live with the wrong abstraction. The technical solution was understood by 2002 at the latest, when Joda-Time published it. JavaScript’s delay is not a technical story; it is a governance story. TC39 requires consensus across browser vendors, and a proposal of this size requires both sustained champion effort and substantial funding. Bloomberg provided both, starting around 2018.

The lesson is not specific to JavaScript. Every standards process has a version of this dynamic: the right answer is knowable early, but implementation requires organizational commitment that volunteer effort cannot always supply. Temporal is a case where corporate sponsorship and standards governance eventually produced the right outcome. The wait was long enough that an entire generation of JavaScript developers learned to treat broken date handling as a fact of life rather than a solvable problem. It is solvable. It just took three decades and a financial engineering firm to prove it in production.

Was this interesting?