· 7 min read ·

From Joda-Time to Temporal: Why Every Language Has to Fix Datetime Twice

Source: hackernews

In 1995, JavaScript copied Date from Java’s java.util.Date. Two years later, Java deprecated most of its own Date API. The language had already moved on, but JavaScript couldn’t follow. Over the next three decades, JavaScript developers lived with what Java itself had abandoned, while Java, Python, and C# each went through their own painful process of arriving at the same conclusion: a single datetime type is the wrong model, and fixing it requires a type hierarchy, not a better API on a bad foundation.

Bloomberg’s retrospective on the Temporal proposal describes nine years of TC39 work to finally fix this in JavaScript. That figure is easier to contextualize once you see how long the same journey took in every other language, and what they all converged on once they got there.

Java: The Original Mistake and Its Fix

Java shipped java.util.Date in 1995. It was immediately recognized as inadequate. The month index started at zero, the year was stored as an offset from 1900, the object was mutable, and DST handling was tied to the local system timezone with no mechanism to specify anything else.

java.util.Calendar followed in 1997, intended as the fix. It addressed some issues but was harder to use. Creating a calendar date required multiple lines of setup code; the API was verbose without being more correct. The methods remained mutable, timezone handling still had edge cases, and the design introduced new inconsistencies of its own.

Joda-Time, written by Stephen Colebourne and first published around 2002, was the real fix. It introduced the type separation that became the template for every subsequent solution: LocalDate for calendar dates without time-of-day, LocalDateTime for dates with time-of-day but no timezone, DateTime for the fully timezone-aware combination, and Instant for raw timestamps. The key insight was that these are not the same thing, and that conflating them under one type makes it impossible to write correct code without extraordinary care.

// java.time (Java 8), based on Joda-Time's design
LocalDate date   = LocalDate.of(2026, 3, 11);       // just a date
LocalDateTime dt = LocalDateTime.of(2026, 3, 11, 9, 0); // date + time, no tz
ZonedDateTime zdt = ZonedDateTime.of(dt, ZoneId.of("America/New_York")); // full
Instant ts       = Instant.now();                       // pure timestamp

Java 8 (2014) incorporated Joda-Time’s design as JSR-310, the java.time package. Colebourne was the JSR-310 spec lead. The old java.util.Date remains for backwards compatibility but is effectively deprecated; any new code reaching for it is doing something wrong. The migration took Java from 1995 to 2014, nineteen years, with community libraries bridging the gap for most of that span.

Python: A Closer Call

Python’s datetime module is better designed than Java’s original, but it contains a fundamental ambiguity. A datetime object can be either “naive” (no timezone information) or “aware” (timezone attached). The distinction is a runtime attribute, not a type distinction. A naive datetime and an aware datetime look identical in type signatures; the difference is invisible until you try to mix them, at which point you get a TypeError at runtime rather than any static guarantee.

from datetime import datetime, timezone

naive = datetime(2026, 3, 11, 9, 0)
aware = datetime(2026, 3, 11, 9, 0, tzinfo=timezone.utc)

aware - naive  # TypeError: can't subtract offset-naive and offset-aware datetimes

The error message is correct but arrives late. The pytz library became standard for timezone-aware work for years, handling DST transitions more carefully than naive datetime arithmetic. Python 3.9 added the zoneinfo module (PEP 615), which uses the system’s IANA timezone database and is now the recommended approach.

Python never completed the structural fix Java made. There is no separate LocalDate type in the standard library, no Instant distinct from an aware datetime. The tools are better than they were, but the core type conflation remains. Libraries like Pendulum apply a stricter approach, but the stdlib stays backward compatible and the structural problem stays with it.

C#: The Library That Never Got Standardized

C#‘s DateTime has a Kind property that can be Unspecified, Utc, or Local. This is structurally similar to Python’s naive/aware distinction: a runtime flag on a single type rather than a type hierarchy. DateTimeOffset adds explicit offset tracking but still does not carry a timezone identifier, so it cannot reason about DST.

Noda Time, written by Jon Skeet, is the C# equivalent of Joda-Time: LocalDate, LocalDateTime, ZonedDateTime, Instant, Duration, Period. Skeet was explicit that the design followed Joda-Time closely. He built it because .NET’s datetime types were causing exactly the same class of bugs that motivated every other library author.

The difference from Java’s story: C# never did a JSR-310. Noda Time remains a third-party package. DateTime is still the standard library type, still has the same structural problem, and new C# developers still discover on their own that DateTime.Kind == DateTimeKind.Unspecified is where bugs live. The Temporal proposal’s champions cited Noda Time as a direct design reference; Jon Skeet’s extensive documentation on the type concepts maps almost directly to Temporal’s. But C# developers who need these guarantees must opt into a library, not the stdlib.

JavaScript: The Slowest Fix, the Biggest Ecosystem

JavaScript inherited Java’s original mistake and held onto it longer than any other major language. The library history follows the same arc: Moment.js (2011) gave the community immutable-ish operations and a duration type built on top of Date; Luxon (2017) redesigned Moment’s approach using Intl.DateTimeFormat for timezone data instead of a bundled database; date-fns took a functional approach to the same broken foundation.

The Temporal proposal started around 2017. Bloomberg funded Igalia to write the formal specification, driven by the same financial date arithmetic problems (DST-correct scheduled events, “9:30 AM New York time every trading day”) that motivated every prior library author. The TC39 champions, Maggie Pint and Matt Johnson-Pint, had built Moment.js and understood precisely what you cannot fix from userland.

The result maps directly to the Joda-Time pattern:

const ts   = Temporal.Instant.from('2026-03-11T14:00:00Z');
const date = Temporal.PlainDate.from('2026-03-11');
const dt   = Temporal.PlainDateTime.from('2026-03-11T09:00:00');
const zdt  = Temporal.ZonedDateTime.from(
  '2026-03-11T09:00:00-05:00[America/New_York]'
);

The names differ slightly from Java’s, but the concepts are identical. Temporal.Instant is java.time.Instant. Temporal.PlainDate is java.time.LocalDate. Temporal.ZonedDateTime is java.time.ZonedDateTime. Noda Time users will find nothing surprising at all.

Temporal is now shipping in Firefox (since version 139) and has landed in V8. For projects that need it today, the @js-temporal/polyfill package provides complete coverage and passes the full TC39 test262 suite.

npm install @js-temporal/polyfill

What the Convergence Means

Every language that has seriously addressed this problem has arrived at the same type hierarchy. That convergence is not coincidental; it is the shape of the problem.

The fundamental error in every broken stdlib was treating “a moment in time” and “a date on a calendar” as the same concept. They are related but distinct. An instant (Temporal.Instant, java.time.Instant, Noda Time’s Instant) is a position on the universal timeline, independent of any calendar or timezone. A local date (Temporal.PlainDate, java.time.LocalDate, Noda Time’s LocalDate) is a position on a human calendar, meaningful only relative to a calendar system and a locale. You need both, and you need them to be different types, because conflating them makes it impossible to express important distinctions without out-of-band conventions.

The secondary lesson is in how languages eventually fix this. Java and JavaScript both did it through the same mechanism: a library author built the right abstraction, the community converged on it, and the language eventually standardized it. C# skipped the last step. The library exists, the design is right, but you still have to opt into it. Python is somewhere in between, with improved tooling but the structural conflation still present in the stdlib.

What took so long in every case was not ignorance. The problem was well-understood. The constraint was backward compatibility. Every language that got this right shipped the new types alongside the old broken ones and accepted that migration would take years. Java still includes java.util.Date. JavaScript will include Date indefinitely. The old types do not disappear; they just gradually stop being what you reach for in new code.

One thing JavaScript did differently from all its predecessors: the Temporal polyfill was production-grade and in use before Stage 3, which meant developers could find real usability problems before the specification was finalized. Java’s migration from java.util.Date to java.time was designed in a committee room and revealed its rough edges after shipping. The Temporal API changed substantially based on polyfill feedback before reaching Stage 3 in 2021. That iteration cost time, but it produced a spec that has been field-tested at a level neither Joda-Time nor java.time could claim at the same stage.

Nine years for one committee covering one language is a long time. Thirty years for the entire industry to converge on the same answer across every major language is a longer one. The answer was always the same type hierarchy. The cost was always backward compatibility and the time required to iterate toward a design that serves the whole language, not just one team’s use cases.

Was this interesting?