coderain guide

Understanding JavaScript's Temporal API: Working with Dates and Times

For decades, JavaScript developers have relied on the `Date` object to handle dates and times. While functional, `Date` is plagued with quirks that make even simple tasks error-prone: mutability, inconsistent parsing, ambiguous time zone handling, and a lack of support for date-only or time-only values. Enter the **Temporal API**—a long-awaited proposal designed to replace `Date` with a modern, intuitive, and robust system for working with time in JavaScript. Temporal addresses the pain points of `Date` by introducing immutable types, clear separation of date/time concepts (e.g., "date without time" vs. "time with time zone"), native time zone and calendar support, and reliable parsing/formatting. Whether you’re building a scheduling app, handling time-sensitive data, or simply calculating durations, Temporal simplifies complex temporal logic. In this guide, we’ll explore Temporal’s core concepts, key objects, and practical use cases, equipping you to migrate from `Date` and leverage Temporal’s power.

Table of Contents

  1. What is the Temporal API?
  2. Core Temporal Objects
  3. Parsing and Formatting
  4. Manipulating Dates and Times
  5. Comparing Temporal Objects
  6. Migrating from Date to Temporal
  7. Advanced Use Cases
  8. Browser Support and Polyfills
  9. Reference

What is the Temporal API?

The Temporal API is a TC39 proposal (currently at Stage 3) aimed at overhauling JavaScript’s date-time handling. Its design goals include:

  • Immutability: All Temporal objects are immutable, preventing accidental side effects.
  • Clarity: Distinct types for different time concepts (e.g., ZonedDateTime for time-zone-aware times, Date for date-only values).
  • Precision: Nanosecond-level accuracy (vs. Date’s millisecond limit).
  • Robustness: Consistent parsing, native time zone support, and non-Gregorian calendar compatibility.

Core Temporal Objects

Temporal introduces a suite of specialized objects to model different aspects of time. Let’s explore the most commonly used ones.

Temporal.Instant: A Global Moment

An Instant represents a specific moment in time (e.g., “July 20, 1969, 20:17:40 UTC”), independent of time zones or calendars. It’s analogous to a Unix timestamp but with nanosecond precision.

Key Features:

  • Immutable.
  • Represents time as the number of nanoseconds since the Unix epoch (January 1, 1970, UTC).

Creating an Instant

// From an ISO 8601 string (UTC)
const moonLanding = Temporal.Instant.from('1969-07-20T20:17:40Z');
console.log(moonLanding.toString()); // "1969-07-20T20:17:40Z"

// From epoch milliseconds (e.g., Date.now())
const nowMs = Date.now(); // 1695000000000
const nowInstant = Temporal.Instant.fromEpochMilliseconds(nowMs);

// From epoch nanoseconds (for precision)
const preciseInstant = Temporal.Instant.fromEpochNanoseconds(1695000000000000000n);

Common Operations

Convert an Instant to a time-zone-specific ZonedDateTime:

const nycTimeZone = Temporal.TimeZone.from('America/New_York');
const moonLandingInNYC = moonLanding.toZonedDateTimeISO(nycTimeZone);
console.log(moonLandingInNYC.toString()); // "1969-07-20T16:17:40-04:00[America/New_York]"

Temporal.ZonedDateTime: Time in a Time Zone

A ZonedDateTime combines an Instant, a TimeZone, and a Calendar to represent a date and time in a specific time zone. Use it when you need to work with wall-clock time in a geographic region (e.g., “3:00 PM in Tokyo”).

Creating a ZonedDateTime

// From an ISO string with time zone
const meeting = Temporal.ZonedDateTime.from('2023-10-05T15:00:00+09:00[Asia/Tokyo]');

// From components (year, month, day, etc.)
const birthday = Temporal.ZonedDateTime.from({
  year: 1990,
  month: 5,
  day: 15,
  hour: 9,
  minute: 30,
  timeZone: 'Europe/Paris', // IANA time zone identifier
  calendar: 'iso8601' // Default: Gregorian calendar
});
console.log(birthday.toString()); // "1990-05-15T09:30:00+02:00[Europe/Paris]"

Manipulating ZonedDateTime

Add/subtract time using add() or subtract() (returns a new ZonedDateTime):

const nextMeeting = meeting.add({ weeks: 1, hours: 2 });
console.log(nextMeeting.toString()); // "2023-10-12T17:00:00+09:00[Asia/Tokyo]"

const prevMeeting = meeting.subtract(Temporal.Duration.from({ days: 3 }));

Temporal.DateTime: Date and Time (No Time Zone)

A DateTime represents a date and time without a time zone (e.g., “2023-12-25T09:00:00”). Use it for abstract “wall-clock” times (e.g., “9 AM on Christmas”) that aren’t tied to a specific geographic region.

Example

// From ISO string
const holiday = Temporal.DateTime.from('2023-12-25T09:00:00');

// From components
const deadline = Temporal.DateTime.from({
  year: 2023,
  month: 12,
  day: 31,
  hour: 23,
  minute: 59
});

// Convert to ZonedDateTime by adding a time zone
const deadlineInLA = deadline.toZonedDateTimeISO('America/Los_Angeles');

Temporal.Date: Date-Only Values

A Date represents a calendar date (year, month, day) without time or time zone (e.g., “2023-10-05”). Use it for date-specific logic like “birthdays” or “deadlines” where time is irrelevant.

Example

const today = Temporal.Now.date(); // Current date (local time zone)
const eventDate = Temporal.Date.from('2024-02-29'); // Leap day
console.log(eventDate.isValid); // true (2024 is a leap year)

// Add 1 month (handles edge cases like January 31 → February 29/28)
const nextMonth = eventDate.add({ months: 1 });
console.log(nextMonth.toString()); // "2024-03-29" (not March 31!)

Temporal.Time: Time-Only Values

A Time represents a time of day (hours, minutes, seconds, nanoseconds) without a date or time zone (e.g., “08:30:00”). Use it for recurring events like “daily at 8:30 AM”.

Example

const alarm = Temporal.Time.from('08:30:00');
const openingTime = Temporal.Time.from({ hour: 9, minute: 0 });

// Check if a time is before another
console.log(alarm.isBefore(openingTime)); // true (8:30 AM < 9:00 AM)

Temporal.Duration: Lengths of Time

A Duration represents a length of time (e.g., “2 days and 3 hours”) rather than a specific moment. Use it to add/subtract time from temporal objects or calculate differences between dates.

Creating and Using Duration

// Create a duration
const tripLength = Temporal.Duration.from({ days: 5, hours: 6 });

// Add to a ZonedDateTime
const departure = Temporal.ZonedDateTime.from('2023-10-01T08:00:00[America/Chicago]');
const arrival = departure.add(tripLength);
console.log(arrival.toString()); // "2023-10-06T14:00:00-05:00[America/Chicago]"

// Calculate difference between two dates
const start = Temporal.Date.from('2023-01-01');
const end = Temporal.Date.from('2023-12-31');
const yearDuration = end.since(start);
console.log(yearDuration.toString()); // "P364D" (364 days in 2023, a non-leap year)

Temporal.TimeZone: Time Zone Logic

A TimeZone object provides tools to work with time zones, including converting between Instant and local time, handling daylight saving time (DST), and fetching time zone offsets.

Example

const berlinTZ = Temporal.TimeZone.from('Europe/Berlin');

// Get the current offset from UTC (in minutes)
const now = Temporal.Now.instant();
const offset = berlinTZ.getOffsetNanosecondsFor(now);
console.log(offset / 3.6e12); // 3600 (seconds → 1 hour ahead of UTC in DST)

// List all IANA time zones (for validation)
console.log(Temporal.TimeZone.from('invalid').toString()); // Throws error!

Temporal.Calendar: Non-Gregorian Calendars

Temporal natively supports non-Gregorian calendars (e.g., Islamic, Hebrew, Japanese) via the Calendar object. Use it to work with dates in cultural or regional contexts.

Example

// Japanese calendar ( Reiwa era)
const japaneseCalendar = Temporal.Calendar.from('japanese');
const todayInJapan = Temporal.Now.date({ calendar: japaneseCalendar });
console.log(todayInJapan.toString()); // "R05-10-05" (Reiwa 5, October 5th)

// Hebrew calendar (Jewish)
const hebrewDate = Temporal.Date.from({
  year: 5784,
  month: 1,
  day: 1,
  calendar: 'hebrew'
});
console.log(hebrewDate.toString()); // "5784-01-01[hebrew]"

Parsing and Formatting

Temporal eliminates Date’s inconsistent parsing by strictly adhering to ISO 8601 and supporting explicit format options.

Parsing Strings

Temporal parses ISO 8601 strings reliably and rejects ambiguous formats:

// Valid: ISO 8601 with time zone
Temporal.ZonedDateTime.from('2023-10-05T15:00:00+02:00[Europe/Berlin]');

// Valid: Date-only
Temporal.Date.from('2023-13-01'); // Throws! (Month 13 is invalid)

// Invalid: Ambiguous non-ISO format (rejected by default)
Temporal.Date.from('10/05/2023'); // Throws! Use options for custom formats.

For non-ISO formats, use Temporal.PlainDate.from() with a parser (via libraries like @js-temporal/parse for now, as native custom parsing is planned).

Formatting

Format Temporal objects using toLocaleString() for human-readable output:

const concert = Temporal.ZonedDateTime.from('2024-02-14T20:00:00[America/Mexico_City]');

// Format for Spanish (Mexico)
console.log(concert.toLocaleString('es-MX', {
  dateStyle: 'full',
  timeStyle: 'long'
})); 
// "miércoles, 14 de febrero de 2024 20:00:00 hora estándar central"

Manipulating Dates and Times

Temporal makes adding/subtracting time intuitive and error-free, even with edge cases like leap years or DST transitions.

Adding/Subtracting Time

const event = Temporal.ZonedDateTime.from('2023-03-12T02:30:00[America/New_York]');

// Add 1 hour during DST transition (clocks spring forward 1 hour)
const adjustedEvent = event.add({ hours: 1 });
console.log(adjustedEvent.toString()); // "2023-03-12T04:30:00-04:00[America/New_York]"
// (3:30 AM is skipped; Temporal adjusts to 4:30 AM)

Comparing Temporal Objects

Temporal objects support direct comparison with compare(), isBefore(), isAfter(), and equals().

Example

const start = Temporal.DateTime.from('2023-10-05T09:00:00');
const end = Temporal.DateTime.from('2023-10-05T17:00:00');
const now = Temporal.Now.dateTime();

if (now.isBetween(start, end)) {
  console.log("Business hours!");
}

Migrating from Date to Temporal

Replace common Date patterns with Temporal equivalents:

TaskDate ApproachTemporal Approach
Current time (UTC)new Date().toISOString()Temporal.Now.instant().toString()
Current local datenew Date().toLocaleDateString()Temporal.Now.date().toString()
Add 1 dayconst d = new Date(); d.setDate(d.getDate() + 1);Temporal.Now.date().add({ days: 1 })
Parse ISO stringnew Date('2023-10-05')Temporal.Date.from('2023-10-05')

Advanced Use Cases

Calculating Overlapping Time Intervals

Check if two events overlap:

const meeting1 = {
  start: Temporal.ZonedDateTime.from('2023-10-05T10:00:00[UTC]'),
  end: Temporal.ZonedDateTime.from('2023-10-05T11:00:00[UTC]')
};

const meeting2 = {
  start: Temporal.ZonedDateTime.from('2023-10-05T10:30:00[UTC]'),
  end: Temporal.ZonedDateTime.from('2023-10-05T12:00:00[UTC]')
};

const overlap = meeting1.start.isBefore(meeting2.end) && 
                meeting2.start.isBefore(meeting1.end);
console.log(overlap); // true (meetings overlap)

Billing Cycles with Durations

Calculate the end of a 30-day billing cycle:

const billingStart = Temporal.Now.zonedDateTimeISO();
const billingEnd = billingStart.add(Temporal.Duration.from({ days: 30 }));
console.log(`Billing ends: ${billingEnd.toLocaleString()}`);

Browser Support and Polyfills

As of 2023, Temporal is a Stage 3 proposal and not yet implemented in browsers. To use it today:

  • Polyfill: Install @js-temporal/polyfill (maintained by the Temporal team):
    npm install @js-temporal/polyfill
  • Browser Support: Track progress on Can I Use.

Reference

With Temporal, JavaScript finally has a date-time API that prioritizes clarity, reliability, and developer experience. Say goodbye to Date-induced headaches and hello to precise, intuitive time handling! 🕒