The Evolution of JavaScript’s Date and Time Handling: From Date API to Temporal

The landscape of handling dates and times in JavaScript has undergone a significant transformation, marked by the evolution from the rudimentary built-in Date API to the widely adopted Moment.js library, and now culminating in the introduction of the standardized Temporal API. This new standard is poised to address the inherent limitations of the original Date API while rectifying the shortcomings and complexities encountered with Moment.js and similar libraries. Joe Attardi’s practical guidance offers invaluable "recipes" for developers seeking to migrate their existing Moment-based codebases to the robust and modern Temporal API.
For nearly every application developed in JavaScript, the management of time and date information is a fundamental requirement. Initially, developers relied solely on the built-in Date API. While this provided basic functionality, its capabilities were notably restricted, often leading to cumbersome workarounds for more complex date and time operations. This paved the way for third-party libraries, with Moment.js emerging as a dominant force, and later, the integration of built-in APIs such as the Intl APIs and the nascent Temporal API, which collectively offer vastly enhanced flexibility in temporal data manipulation.
The Rise and Fall of Moment.js
Moment.js, a JavaScript library, rose to prominence by providing a comprehensive suite of utilities for working with dates and times. It adeptly filled the gaps left by the native Date API, offering crucial features like time zone manipulation and simplifying numerous common operations. Its ability to format dates and times with ease further cemented its status as a de facto standard, leading to its widespread adoption across a vast array of applications.
However, Moment.js was not without its drawbacks. Its considerable size contributed significantly to application bundle sizes. Compounding this issue, the library did not support tree shaking – a vital feature of modern bundlers that allows for the removal of unused library code. Consequently, even if an application utilized only a fraction of Moment.js’s functionalities, the entire library was bundled, leading to an unnecessarily inflated footprint.
Another significant concern with Moment.js was the mutability of the objects it generated. The execution of certain functions on a Moment object could result in unintended side effects, altering the object’s original value and potentially introducing subtle bugs or unpredictable behavior.
Recognizing these challenges and the evolving needs of the JavaScript ecosystem, the maintainers of Moment.js made the decision in 2020 to transition the library into maintenance mode. This meant that no new feature development would occur, and developers were advised against using it for new projects, signaling a clear shift towards more modern and efficient solutions. While other JavaScript date libraries like date-fns continued to offer viable alternatives, the advent of Temporal, an API being directly integrated into the ECMAScript standard, presented a groundbreaking development. Temporal aims to not only patch the deficiencies of the original Date API but also to resolve the limitations identified in Moment.js and other libraries.
What is Temporal?
Temporal is a new time and date API being incorporated into the ECMAScript standard, the specification that governs modern JavaScript. As of March 2026, Temporal has successfully navigated through Stage 4 of the TC39 process – the committee responsible for proposals and additions to the JavaScript language – ensuring its inclusion in the forthcoming ECMAScript specification. Its integration into major browsers is already well underway, with Chrome version 144+ and Firefox version 139+ already featuring robust implementations. Safari is also expected to follow suit shortly. For developers requiring immediate access or needing to support older browsers and Node.js environments, a comprehensive polyfill is readily available.
At its core, the Temporal API is designed to create objects that accurately represent moments in time. These can encompass full date and time stamps within a specific time zone, or more generalized "wall clock" time instances devoid of explicit time zone or date context. Key features of the Temporal API include enhanced precision, improved handling of time zones and daylight saving time, and a more intuitive and predictable API design.
It is crucial to note that the original Date API is not being deprecated or removed. While Temporal offers a superior and more comprehensive approach to date and time management, the existing Date API will remain accessible to ensure backward compatibility and prevent widespread application breakages. However, the industry consensus is clear: Moment.js is now considered a legacy project, and Temporal represents the future of date and time handling in JavaScript. The subsequent sections will delve into practical "recipes" for migrating existing Moment-based code to the Temporal API, commencing the refactoring process.
Creating Date and Time Objects
The foundational step in manipulating dates and times involves creating objects that accurately represent them. In the Moment.js ecosystem, creating an object for the current date and time is achieved through the moment() function.
const now = moment();
console.log(now);
// Moment<2026-02-18T21:26:29-05:00>
This Moment object can then be subjected to various formatting or manipulation operations.
// Convert to UTC
// Warning: This mutates the Moment object and puts it in UTC mode!
console.log(now.utc());
// Moment<2026-02-19T02:26:29Z>
// Print a formatted string - note that it's using the UTC time now
console.log(now.format('MM/DD/YYYY hh:mm:ss a'));
// 02/19/2026 02:27:07 am
A critical aspect to remember about Moment.js is that each Moment object inherently encapsulates both date and time information. While this is often acceptable, it can lead to unexpected behavior in scenarios involving Daylight Saving Time transitions or leap years, where the date can significantly influence time calculations.
Temporal offers a more granular and flexible approach. To capture the current date and time, one can instantiate a Temporal.Instant object. This object represents a precise point in time, measured from the Unix epoch (midnight UTC on January 1, 1970). Temporal provides nanosecond-level precision for these instants.
const now = Temporal.Now.instant();
// See raw nanoseconds since the epoch
console.log(now.epochNanoseconds);
// 1771466342612000000n
// Format for UTC
console.log(now.toString());
// 2026-02-19T01:55:27.844Z
// Format for a particular time zone
console.log(now.toString( timeZone: 'America/New_York' ));
// 2026-02-18T20:56:57.905-05:00
Temporal.Instant objects can also be constructed for specific dates and times using the static from method.
const myInstant = Temporal.Instant.from('2026-02-18T21:10:00-05:00');
// Format the instant in the local time zone. Note that this only controls
// the formatting - it does not mutate the object like `moment.utc` does.
console.log(myInstant.toString( timeZone: 'America/New_York' ));
// 2026-02-18T21:10:00-05:00
Temporal also provides a richer set of object types to represent different temporal concepts:
Temporal.PlainDate: Represents a date without a time zone or time.Temporal.PlainTime: Represents a time without a date or time zone.Temporal.PlainDateTime: Represents a date and time without a time zone.Temporal.ZonedDateTime: Represents a specific date and time within a defined time zone.Temporal.TimeZone: Represents a specific time zone.Temporal.Duration: Represents a time span.
Each of these object types features a from method that accepts an object specifying the date and/or time components, or a date string for parsing.
// Just a date
const today = Temporal.PlainDate.from(
year: 2026,
month: 2, // note we're using 2 for February
day: 18
);
console.log(today.toString());
// 2026-02-18
// Just a time
const lunchTime = Temporal.PlainTime.from(
hour: 12
);
console.log(lunchTime.toString());
// 12:00:00
// A date and time in the US Eastern time zone
const dueAt = Temporal.ZonedDateTime.from(
timeZone: 'America/New_York',
year: 2026,
month: 3,
day: 1,
hour: 12,
minute: 0,
second: 0
);
console.log(dueAt.toString());
// 2026-03-01T12:00:00-05:00[America/New_York]
Parsing Date Strings
Parsing date strings is an area where Moment.js exhibits greater flexibility than the core Temporal API. With Moment.js, a date string can be parsed by passing it to the moment() function. While Moment.js defaults to expecting an ISO date string when provided with a single argument, it can accommodate alternative formats by specifying a second argument that defines the expected date format.
const isoDate = moment('2026-02-21T09:00:00');
const formattedDate = moment('2/21/26 9:00:00', 'M/D/YY h:mm:ss');
console.log(isoDate);
// Moment<2026-02-21T09:00:00-05:00>
console.log(formattedDate);
// Moment<2026-02-21T09:00:00-05:00>
Historically, Moment.js would attempt to infer the format of arbitrarily formatted date strings, which could lead to ambiguous or incorrect parsing. For instance, the string 02-03-2026 could be interpreted as either February 3rd or March 2nd. To mitigate this ambiguity, newer versions of Moment.js issue a prominent deprecation warning if called with a non-ISO formatted date string, unless an explicit format is provided.
Temporal, in contrast, adheres strictly to parsing specifically formatted date strings, primarily those compliant with the ISO 8601 standard or its extension, RFC 9557. If a non-compliant date string is passed to a from method, Temporal will raise a RangeError.
// Using an RFC 9557 date string
const myDate = Temporal.Instant.from('2026-02-21T09:00:00-05:00[America/New_York]');
console.log(myDate.toString( timeZone: 'America/New_York' ));
// 2026-02-21T09:00:00-05:00
// Using an unknown date string
const otherDate = Temporal.Instant.from('2/21/26 9:00:00');
// RangeError: Temporal error: Invalid character while parsing year value.
The precise formatting requirements for date strings in Temporal vary depending on the specific Temporal object being created. For instance, Temporal.Instant necessitates a complete ISO 8601 or RFC 9557 string, including date, time, and time zone offset. However, subsets of this format can be used for PlainDate or PlainTime objects.
const myDate = Temporal.PlainDate.from('2026-02-21');
console.log(myDate.toString());
// 2026-02-21
const myTime = Temporal.PlainTime.from('09:00:00');
console.log(myTime.toString());
// 09:00:00
It is important to reiterate that these strings must still adhere to the expected format; otherwise, a RangeError will be thrown.
// Using non-compliant time strings. These will all throw a RangeError.
// Temporal.PlainTime.from('9:00');
// Temporal.PlainTime.from('9:00:00 AM');
Pro tip: Handling non-ISO strings
Given Temporal’s emphasis on reliability and explicitness, it will not attempt to guess the format of strings like 02-01-2026. If your data sources employ such non-standard formats, it is imperative to perform string manipulation to rearrange the values into an ISO-compliant string, such as 2026-02-01, before attempting to parse it with Temporal.
Formatting Date and Time Output
Once date and time objects are created, the next common requirement is to convert them into human-readable, formatted strings. Moment.js offers a concise method for this: the format() method, which takes a string of tokens defining the desired output.
const date = moment();
console.log(date.format('MM/DD/YYYY'));
// 02/22/2026
console.log(date.format('MMMM Do YYYY, h:mm:ss a'));
// February 22nd 2026, 8:18:30 pm
Temporal, on the other hand, employs a more verbose approach using the toLocaleString() method, which accepts an object containing various formatting options.
const date = Temporal.Now.instant();
// with no arguments, we'll get the default format for the current locale
console.log(date.toLocaleString());
// 2/22/2026, 8:23:36 PM (assuming a locale of en-US)
// pass formatting options to generate a custom format string
console.log(date.toLocaleString('en-US',
month: 'long',
day: 'numeric',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
));
// February 22, 2026 at 8:23 PM
// only pass the fields you want in the format string
console.log(date.toLocaleString('en-US',
month: 'short',
day: 'numeric'
));
// Feb 22
Crucially, Temporal’s date formatting leverages the Intl.DateTimeFormat API, which is natively supported by modern browsers. This means developers can create reusable DateTimeFormat objects with custom formatting preferences and then apply them to Temporal objects via their format() method. This approach, however, means Temporal does not support custom, arbitrary date formats in the same way Moment.js does. For specialized formats like 'Q1 2026' or other unique requirements, custom date formatting logic or the use of a third-party library may be necessary.
const formatter = new Intl.DateTimeFormat('en-US',
month: '2-digit',
day: '2-digit',
year: 'numeric'
);
const date = Temporal.Now.instant();
console.log(formatter.format(date));
// 02/22/2026
While Moment.js’s formatting tokens are simpler to write, they lack inherent locale-friendliness. The format strings "hard code" elements like month/day order, making them less adaptable to different regional conventions. Temporal’s use of configuration objects for formatting offers a significant advantage: it automatically adapts to any specified locale, applying the correct formatting conventions.

const date = Temporal.Now.instant();
const formatOptions =
month: 'numeric',
day: 'numeric',
year: 'numeric'
;
console.log(date.toLocaleString('en-US', formatOptions));
// 2/22/2026
console.log(date.toLocaleString('en-GB', formatOptions));
// 22/02/2026
Date Calculations
A common requirement in many applications involves performing calculations on dates, such as adding or subtracting units of time (days, hours, seconds, etc.). For instance, from the current date, one might need to display the date one week in the future.
Moment.js provides methods like add() and subtract() for these operations. These functions typically take a value and a unit, such as add(7, 'days'). A critical distinction between Moment.js and Temporal lies in the fact that Moment’s date calculation methods modify the underlying object directly, leading to the loss of its original value.
const now = moment();
console.log(now);
// Moment<2026-02-24T20:08:36-05:00>
const nextWeek = now.add(7, 'days');
console.log(nextWeek);
// Moment<2026-03-03T20:08:36-05:00>
// Gotcha - the original object was mutated
console.log(now);
// Moment<2026-03-03T20:08:36-05:00>
To circumvent the loss of the original date value, Moment.js developers must explicitly call the clone() method on the Moment object before performing any modification.
const now = moment();
const nextWeek = now.clone().add(7, 'days');
console.log(now);
// Moment<2026-02-24T20:12:55-05:00>
console.log(nextWeek);
// Moment<2026-03-03T20:12:55-05:00>
Temporal objects, conversely, are immutable. Once an object such as an Instant, PlainDate, or ZonedDateTime is created, its value remains constant. Temporal’s add() and subtract() methods operate on these immutable objects by returning new objects that represent the result of the calculation.
Temporal exhibits a degree of strictness regarding which time units can be added to specific object types. For example, attempting to add days to an Instant object will result in a RangeError.
const now = Temporal.Now.instant();
// const nextWeek = now.add( days: 7 ); // This will throw a RangeError
// RangeError: Temporal error: Largest unit cannot be a date unit
This restriction arises because Instant objects represent precise points in time in UTC and are inherently calendar-agnostic. Since the duration of a day can fluctuate due to time zone rules like Daylight Saving Time, such calculations are not permissible on an Instant. However, these operations are fully supported on other Temporal object types, such as PlainDateTime.
const now = Temporal.Now.plainDateTimeISO();
console.log(now.toLocaleString());
// 2/24/2026, 8:23:59 PM
const nextWeek = now.add( days: 7 );
// Note that the original PlainDateTime remains unchanged
console.log(now.toLocaleString());
// 2/24/2026, 8:23:59 PM
console.log(nextWeek.toLocaleString());
// 3/3/2026, 8:23:59 PM
Both Moment.js and Temporal enable the calculation of the time difference between two temporal objects. Moment.js’s diff() function requires a unit of granularity; otherwise, it returns the difference in milliseconds.
const date1 = moment('2026-02-21T09:00:00');
const date2 = moment('2026-02-22T10:30:00');
console.log(date2.diff(date1));
// 91800000
console.log(date2.diff(date1, 'days'));
// 1
To achieve a similar result with Temporal, one can pass another Temporal object to its until() or since() methods. These methods return a Temporal.Duration object, which encapsulates the details of the time difference. The Duration object provides properties for each component of the difference and can also generate an ISO 8601 duration string.
const date1 = Temporal.PlainDateTime.from('2026-02-21T09:00:00');
const date2 = Temporal.PlainDateTime.from('2026-02-22T10:30:00');
// largestUnit specifies the largest unit of time to represent
// in the duration calculation
const diff = date2.since(date1, largestUnit: 'day' );
console.log(diff.days);
// 1
console.log(diff.hours);
// 1
console.log(diff.minutes);
// 30
console.log(diff.toString());
// P1DT1H30M
// (ISO 8601 duration string: 1 day, 1 hour, 30 minutes)
Comparing Dates and Times
Both Moment.js and Temporal provide mechanisms for comparing dates and times to determine their chronological order. Moment.js offers a set of intuitive methods, including isBefore(), isAfter(), and isSame(), for comparing two Moment objects.
const date1 = moment('2026-02-21T09:00:00');
const date2 = moment('2026-02-22T10:30:00');
console.log(date1.isBefore(date2));
// true
Temporal utilizes a static compare() method for comparisons between objects of the same type. This method returns:
-1if the first date precedes the second.0if the dates are identical.1if the first date follows the second.
The following example demonstrates comparing two PlainDate objects. It is crucial that both arguments passed to Temporal.PlainDate.compare() are PlainDate objects.
const date1 = Temporal.PlainDate.from( year: 2026, month: 2, day: 24 );
const date2 = Temporal.PlainDate.from( year: 2026, month: 3, day: 24 );
// date1 comes before date2, so -1
console.log(Temporal.PlainDate.compare(date1, date2));
// Error if we try to compare two objects of different types
// console.log(Temporal.PlainDate.compare(date1, Temporal.Now.instant()));
// TypeError: Temporal error: Invalid PlainDate fields provided.
This comparison functionality is particularly useful for sorting arrays of Temporal objects chronologically.
// An array of Temporal.PlainDate objects
const dates = [
Temporal.PlainDate.from( year: 2027, month: 1, day: 15 ),
Temporal.PlainDate.from( year: 2026, month: 12, day: 1 ),
Temporal.PlainDate.from( year: 2027, month: 1, day: 10 )
];
// use Temporal.PlainDate.compare as the comparator function
dates.sort(Temporal.PlainDate.compare);
console.log(dates);
// [
// Temporal.PlainDate year: 2026, month: 12, day: 1 ,
// Temporal.PlainDate year: 2027, month: 1, day: 10 ,
// Temporal.PlainDate year: 2027, month: 1, day: 15
// ]
Time Zone Conversions
The core Moment.js library does not natively support time zone conversions. This functionality requires the additional installation of the moment-timezone package, which, being non-tree-shakable, can substantially increase application bundle size. Once installed, time zone conversions are performed using the tz() method. Similar to other Moment.js operations, this method mutates the underlying object.
// Assuming US Eastern time
const now = moment();
console.log(now);
// Moment<2026-02-28T20:08:20-05:00>
// Convert to Pacific time.
// The original Eastern time is lost.
now.tz('America/Los_Angeles');
console.log(now);
// Moment<2026-02-28T17:08:20-08:00>
Time zone functionality is intrinsically integrated into the Temporal API when working with Temporal.ZonedDateTime objects. These objects feature a withTimeZone() method that returns a new ZonedDateTime instance representing the same moment in time but adjusted to the specified time zone.
// Again, assuming US Eastern time
const now = Temporal.Now.zonedDateTimeISO();
console.log(now.toLocaleString());
// 2/28/2026, 8:12:02 PM EST
// Convert to Pacific time
const nowPacific = now.withTimeZone('America/Los_Angeles');
console.log(nowPacific.toLocaleString());
// 2/28/2026, 5:12:02 PM PST
// Original object remains unchanged
console.log(now.toLocaleString());
// 2/28/2026, 8:12:02 PM EST
Note: The formatted values returned by toLocaleString() are locale-dependent. The sample code was developed in the en-US locale, resulting in a format like 2/28/2026, 5:12:02 PM PST. In a different locale, such as en-GB, the output might be 28/2/2026, 17:12:02 GMT-8.
A Real-World Refactoring Example
Consider an application designed for scheduling events across different time zones. A key function within this application, getEventTimes, accepts an ISO 8601 string representing the event’s date and time, the user’s local time zone, and a target time zone. The function then generates formatted date and time strings for the event in both time zones. If an invalid date/time string is provided, the function is designed to throw an error.
Here is an original implementation using Moment.js, which also necessitates the moment-timezone package:
import moment from 'moment-timezone';
function getEventTimes(inputString, userTimeZone, targetTimeZone)
const timeFormat = 'MMM D, YYYY, h:mm:ss a z';
// 1. Create the initial moment in the user's time zone
const eventTime = moment.tz(
inputString,
moment.ISO_8601, // Expect an ISO 8601 string
true, // Strict parsing
userTimeZone
);
// Throw an error if the inputString did not represent a valid date
if (!eventTime.isValid())
throw new Error('Invalid date/time input');
// 2. Calculate the target time
// CRITICAL: We must clone, or 'eventTime' changes forever!
const targetTime = eventTime.clone().tz(targetTimeZone);
return
local: eventTime.format(timeFormat),
target: targetTime.format(timeFormat),
;
const schedule = getEventTimes(
'2026-03-05T15:00-05:00',
'America/New_York',
'Europe/London',
);
console.log(schedule.local);
// Mar 5, 2026, 3:00:00 pm EST
console.log(schedule.target);
// Mar 5, 2026, 8:00:00 pm GMT
In this Moment.js example, the expected date format is ISO 8601, which is conveniently built into the library. Strict parsing is enabled, meaning Moment.js will not attempt to guess the format of a string that deviates from the specified pattern. If a non-ISO date string is supplied, it results in an invalid date object, triggering the error handling.
The Temporal implementation of getEventTimes maintains a similar structure but introduces key differences:
function getEventTimes(inputString, userTimeZone, targetTimeZone)
// 1. Parse the input directly into an Instant, then create
// a ZonedDateTime in the user's zone.
const instant = Temporal.Instant.from(inputString);
const eventTime = instant.toZonedDateTimeISO(userTimeZone);
// 2. Convert to the target zone
// This automatically returns a NEW object; 'eventTime' is safe.
const targetTime = eventTime.withTimeZone(targetTimeZone);
// 3. Format using Intl (built-in)
const options =
year: 'numeric',
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: '2-digit',
second: '2-digit',
timeZoneName: 'short'
;
return
local: eventTime.toLocaleString(navigator.language, options),
target: targetTime.toLocaleString(navigator.language, options)
;
const schedule = getEventTimes(
'2026-03-05T15:00-05:00',
'America/New_York',
'Europe/London',
);
console.log(schedule.local);
// Mar 5, 2026, 3:00:00 PM EST
console.log(schedule.target);
// Mar 5, 2026, 8:00:00 PM GMT
With Moment.js, explicit format strings are required for the output. Regardless of the user’s locale, event times are consistently formatted as Mar 5, 2026, 3:00:00 pm EST.
In contrast, Temporal’s Temporal.Instant.from() method inherently throws an exception for invalid input strings, negating the need for explicit error handling. It’s important to note that Temporal’s parsing is stricter; even with strict parsing enabled in Moment.js, Temporal mandates the inclusion of the time zone offset at the end of the string.
Furthermore, the Temporal implementation utilizes navigator.language for formatting. This means the code will function correctly in a browser environment, automatically adapting the event times to the user’s local display preferences. For instance, in the en-US locale, the output is Mar 5, 2026, 3:00:00 pm EST. However, for a user in London, the formatting would adjust accordingly, potentially displaying 5 Mar 2026, 15:00:00 GMT-5. This dynamic locale adaptation is a significant advantage over Moment.js’s fixed formatting.
Summary Table: Moment.js vs. Temporal
| Action | Moment.js | Temporal |
|---|---|---|
| Current Time | moment() |
Temporal.Now.zonedDateTimeISO() |
| Parsing ISO String | moment(str) |
Temporal.Instant.from(str) |
| Adding Time | .add(7, 'days') (mutates) |
.add( days: 7 ) (new object) |
| Calculating Difference | .diff(other, 'hours') |
.since(other).hours |
| Time Zone Conversion | .tz('Zone/Name') (mutates) |
.withTimeZone('Zone/Name') (new object) |
| Immutability | Mutable objects | Immutable objects |
| Parsing Strictness | Can be lenient, requires format string | Strict, adheres to ISO 8601/RFC 9557 |
| Locale Awareness | Limited, format strings are locale-agnostic | Built-in, uses Intl.DateTimeFormat |
At first glance, the syntax may appear slightly different, and Temporal can sometimes be more verbose and strictly defined. However, the advantages of Temporal over Moment.js are substantial:
- Immutability: Temporal’s immutable objects prevent unintended side effects and make code more predictable and easier to reason about.
- Clarity and Expressiveness: The API is designed to be more explicit and less ambiguous, reducing the likelihood of common errors.
- Performance: Temporal is engineered for better performance and memory management compared to the often bloated Moment.js library.
- Standardization: As a native JavaScript API, Temporal eliminates the need for external dependencies, leading to smaller bundle sizes and improved maintainability.
- Robust Time Zone Handling: Built-in, precise, and efficient time zone management is a core feature, eliminating the need for supplementary libraries.
Notes on the Polyfill
A Temporal polyfill is available via the npm package @js-temporal/polyfill. This is essential for developers who need to utilize Temporal’s capabilities in browsers that have not yet shipped the native API, such as Safari. While the polyfill adds to the bundle size, it is significantly smaller than Moment.js or moment-timezone. According to Bundlephobia.com:
| Package | Minified | Minified & Gzipped |
|---|---|---|
@js-temporal/polyfill |
154.1 kB | 44.1 kB |
moment |
294.4 kB | 75.4 kB |
moment-timezone |
1 MB | 114.2 kB |
Historically, the polyfill has faced some performance challenges, particularly concerning memory usage. At the time of writing, it is considered to be in an alpha state, suggesting caution for production use until it matures further.
The good news is that the reliance on polyfills is expected to diminish as Temporal gains broader native support. Chrome, Edge, and Firefox have already shipped implementations, and Safari’s adoption is anticipated in the near future, further solidifying Temporal’s position as the future standard for date and time management in JavaScript.







