Why Temporal Replaces Moment.js

JavaScript's built-in Date API lacks many features that applications need, which led to the widespread adoption of libraries like Moment.js. Moment added time zone support, formatting utilities, and simpler date arithmetic. But Moment has notable drawbacks: it mutates objects in place, adds significant bundle weight without tree shaking support, and its maintainers put the project into maintenance mode back in 2020.

The new Temporal API, now at Stage 4 of the TC39 process as of March 2026, addresses these issues. It ships natively in Chrome 144+ and Firefox 139+, with Safari expected to support it soon, and a polyfill is available for other environments. Unlike Moment, Temporal objects are immutable, adding nothing to bundle size since it's part of the platform. The existing Date API remains supported and isn't deprecated.

Core Differences In Object Design

Moment objects always include both date and time information, even when you only need one. Temporal separates these into distinct types. A Temporal.Instant represents a fixed point in time measured from the Unix epoch with nanosecond precision. Other types include Temporal.PlainDate for dates only, Temporal.PlainTime for wall-clock times, and Temporal.ZonedDateTime for dates with time zones.

Temporal also fixes a long-standing Date API bug: months are 1-based, so January is month 1 rather than 0.

Creating Objects

Creating a current Moment object involves calling the moment function, and formatting subsequent operations work by calling methods on it.

const now = moment();
console.log(now); 
// Moment<2026-02-18T21:26:29-05:00>
// 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

Temporal offers more variety based on what you need. For the current moment in time, create an Temporal.Instant:

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

To create an Instant at a specific time, use the static from method with an ISO 8601 string, which requires a date, time, and time zone offset:

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

Other Temporal types similarly use their own from methods, accepting either an object with date/time properties or a partial date string:

// 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 Limitations And Solutions

Moment easily parses arbitrary date formats. With a single string argument it assumes ISO format; pass a second format string to parse something like 02-01-2026.

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>

Temporal deliberately takes a stricter approach. It only parses strings that comply with ISO 8601 or its extension RFC 9557. Passing a non-compliant string throws a RangeError. While you can parse a string that contains only a date when creating a PlainDate, the string must still conform to the expected subset of the format.

// 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.
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
// Using a 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

Because Temporal prioritizes reliability, it won't try to guess the format of a string like 02-01-2026. If your data source uses such strings, you will need to do some string manipulation to rearrange the values into an ISO string like 2026-02-01 before attempting to use it with Temporal.

Formatting Output

Moment's format method uses terse token strings to describe the output layout, which is convenient but makes the format locale-independent.

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 objects provide a toLocaleString method that takes a configuration object instead of a format string. Under the hood this uses the Intl.DateTimeFormat API, so formats adapt automatically to the user's locale. For example, month and day ordering follows the locale's conventions rather than being hard-coded in a token string.

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

The integration with Intl means Temporal doesn't accept custom format patterns. Formatting by reusing a configured DateTimeFormat object and passing a Temporal object to its format method works well:

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
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

If you need a specialized output like 'Q1 2026', you'll need custom formatting code or a third-party library, since Temporal won't generate arbitrary formats.

Date Arithmetic And Intervals

Both libraries offer add and subtract methods, but their behavior differs critically. Moment mutates the original object, losing the initial value unless you first call clone to preserve it.

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>
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's immutability means arithmetic operations return a new object while leaving the original untouched. However, Temporal restricts which units you can apply to which types. You cannot add days to an Instant because an Instant is calendar-agnostic and the length of a day varies with time zone rules like Daylight Saving Time. Adding days to a PlainDateTime is allowed, since that type carries calendar context.

const now = Temporal.Now.instant();
const nextWeek = now.add({ days: 7 });
// RangeError: Temporal error: Largest unit cannot be a date unit
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

For measuring the time between two dates, Moment's diff requires a granularity unit or defaults to 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

Temporal offers until and since methods that return a Temporal.Duration object. This object exposes properties for each time component, and also provides ISO 8601 duration strings.

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

Moment and Temporal take fundamentally different approaches to comparison. Moment offers dedicated instance methods like isBefore, isAfter, and isSame:

const date1 = moment('2026-02-21T09:00:00');
const date2 = moment('2026-02-22T10:30:00');

console.log(date1.isBefore(date2));
// true

Temporal instead provides a static compare method that works on two objects of the same type. The method returns -1 when the first object comes before the second, 0 when they are equal, and 1 when the first object comes after the second. Here, both arguments to Temporal.PlainDate.compare must be 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 design makes chronological sorting of arrays straightforward:

// An array of Temporal.PlainDate objects
const dates = [ ... ];

// use Temporal.PlainDate.compare as the comparator function
dates.sort(Temporal.PlainDate.compare);

Handling Time Zones

Time zone support is not part of core Moment. You need the separate moment-timezone package, which is not tree-shakable and can add significantly to your bundle size. With that package installed, the tz method converts a Moment object to another time zone, mutating the original object in the process:

// 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>

Temporal builds time zone handling in via the Temporal.ZonedDateTime type. These objects expose a withTimeZone method that returns a new ZonedDateTime for the same instant in the requested 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: Output from toLocaleString is locale-dependent by design. The sample output here was produced in the en-US locale and appears as 2/28/2026, 5:12:02 PM PST. In en-GB, the same call would produce 28/2/2026, 17:12:02 GMT-8.

Refactoring a Scheduling Function

Consider an app that schedules events across time zones. A helper function, getEventTimes, takes an ISO 8601 string for the event time, a local time zone, and a target time zone, then produces formatted strings for both zones. Invalid input should raise an error.

The Moment version depends on moment-timezone:

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

That code relies on Moment's built-in ISO 8601 support with strict parsing, which prevents Moment from guessing at non-conforming input. A non-ISO date string results in an invalid date object, at which point we throw an error ourselves.

The Temporal equivalent looks similar but differs in important ways:

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, you specify an explicit format token string, so the output always looks like Mar 5, 2026, 3:00:00 pm EST for every user. Temporal removes that concern: it uses the current locale via navigator.language, so a user in London sees 5 Mar 2026, 15:00:00 GMT-5 while one in the US sees the en-US formatting. Note that this also means the code must run in a browser, since navigator is unavailable in Node.js.

You also don't need an explicit throw. Temporal's parse methods reject invalid strings themselves. Still, Moment with strict parsing is more forgiving about the input format itself; Temporal additionally requires the time zone offset at the end of the ISO string.

The Benefits Of Temporal

ActionMoment.jsTemporal
Current timemoment()Temporal.Now.zonedDateTimeISO()
Parsing ISOmoment(str)Temporal.Instant.from(str)
Add time.add(7, 'days') (mutates).add({ days: 7 }) (new object)
Difference.diff(other, 'hours').since(other).hours
Time zone.tz('Zone/Name').withTimeZone('Zone/Name')

At a glance, Temporal's syntax looks more verbose and stricter than Moment's. But those differences are deliberate and bring real advantages:

  • Explicitness reduces surprises. Moment's leniency relies on guesswork that can occasionally produce incorrect dates. Temporal throws on invalid input, so if your code executes, the date is valid.
  • Smaller footprint. Moment—and especially moment-timezone—adds considerable bundle weight. Temporal adds nothing once the API is native in your target browsers.
  • Immutability. Conversions and operations never overwrite existing objects, so you won't accidentally lose data.
  • Precise time models. Instant, PlainDateTime, and ZonedDateTime let you pick the right representation for each job, whereas Moment always wraps a UTC timestamp.
  • Locale-aware formatting. Temporal builds on the Intl API, so localized output requires no manual format tokens.

Polyfill Notes

Until Safari ships the API, the @js-temporal/polyfill npm package is the way to use Temporal today. Yes, it increases bundle size, but as the Bundlephobia figures below show, it remains far lighter than moment or moment-timezone:

PackageMinifiedMinified & gzipped
@js-temporal/polyfill154.1 kB44.1 kB
moment294.4 kB75.4 kB
moment-timezone1 MB114.2 kB

The polyfill is not without caveats. It has shown memory-related performance issues historically and is still considered alpha-quality at the time of writing, so production use may be premature until it matures.

The good news is that the polyfill should soon be unnecessary for modern browsers. Temporal already ships in Chrome, Edge, and Firefox. Safari lags behind, but it appears to be available behind a runtime flag in the latest Technology Preview.

Smashing Editorial