Skip to content
ExponaBLOG
JAVASCRIPT & TYPESCRIPT · DAY 028 / 100

Dates, Intl and Temporal: a hands-on guide for engineers

Time is hard. Use the right tools.

Expona Admin8 min read

Your application works perfectly during testing in New York, but users in Tokyo report that scheduled emails arrive at the wrong time. A payment processor integrates with your API and everything passes validation—until daylight saving time kicks in. Time is hard, and JavaScript's native Date object was never designed for the real world. This guide walks you through dates, Intl and Temporal: a hands-on guide for engineers who need to ship reliable time handling in production.

Date is limited — Mutable, confusing months, weak time zone support

JavaScript's Date object feels intuitive at first but breaks down quickly in production systems. Three core problems plague it: mutability, confusing month indexing, and weak time zone support.

Month indexing is a classic gotcha. Months are zero-indexed, so January is 0 and December is 11. Create a birthday date like this:

typescript
const birthday = new Date(1990, 0, 15); // January 15, 1990
console.log(birthday.getMonth()); // 0
const wrongBirthday = new Date(1990, 1, 15); // Oops, February 15, 1990

Mutability is another hazard. Date objects can be modified after creation, making them unsafe to pass around:

typescript
const meetingTime = new Date('2024-03-15T14:00:00Z');
const eventReference = meetingTime;
eventReference.setHours(9); // Mutates the original
console.log(meetingTime.getHours()); // 9, not 14

Time zone handling is the biggest pain point. Date stores everything as UTC internally, but getHours(), getMinutes(), and related methods operate in the local time zone. There's no built-in way to work with times in a specific time zone—you'll have to calculate offsets manually or rely on external libraries:

typescript
// What time is it in Tokyo?
const now = new Date();
console.log(now.toLocaleString('ja-JP', { timeZone: 'Asia/Tokyo' }));
// This works for display, but you can't reliably do arithmetic in that time zone

For any application that serves users across multiple time zones or handles scheduled operations, Date forces you into workarounds. You'll find yourself storing timestamps as milliseconds since epoch, doing manual time zone conversions, and debugging off-by-one errors in month calculations.

Intl formats — Intl.DateTimeFormat and RelativeTimeFormat for display

While Date struggles with time zones, the Intl API excels at display. Intl.DateTimeFormat and Intl.RelativeTimeFormat handle formatting and localization without external dependencies, and they respect system time zone settings and user locales.

Use Intl.DateTimeFormat for any user-facing date or time display. It handles time zones, locales, and formatting options elegantly:

typescript
const timestamp = new Date('2024-03-15T14:30:00Z');

// Display to a Tokyo user
const tokyoFormatter = new Intl.DateTimeFormat('ja-JP', {
  timeZone: 'Asia/Tokyo',
  year: 'numeric',
  month: '2-digit',
  day: '2-digit',
  hour: '2-digit',
  minute: '2-digit',
  second: '2-digit',
});

console.log(tokyoFormatter.format(timestamp));
// 2024/03/16 23:30:00

// Display to a New York user
const nyFormatter = new Intl.DateTimeFormat('en-US', {
  timeZone: 'America/New_York',
  year: 'numeric',
  month: 'long',
  day: 'numeric',
  hour: 'numeric',
  minute: '2-digit',
});

console.log(nyFormatter.format(timestamp));
// March 15, 2024, 10:30 AM

Intl.RelativeTimeFormat shines for user-friendly relative timestamps—"2 hours ago," "in 3 days," etc. This is far better than building your own relative time logic:

typescript
const rtf = new Intl.RelativeTimeFormat('en-US', { numeric: 'auto' });

console.log(rtf.format(-2, 'hour'));   // 2 hours ago
console.log(rtf.format(3, 'day'));    // in 3 days
console.log(rtf.format(-1, 'month')); // last month

// Localize for other languages
const rtfJa = new Intl.RelativeTimeFormat('ja-JP', { numeric: 'auto' });
console.log(rtfJa.format(-2, 'hour')); // 2時間前

The key principle: use Intl for display only. Keep your internal logic working with UTC timestamps, and convert to user time zones at the presentation layer. This separation makes testing easier and prevents time zone bugs from creeping into business logic.

Temporal is arriving — A modern date-time API with time zones and calendars

Temporal is a Stage 3 TC39 proposal that fixes nearly every pain point in Date. It's immutable, has explicit time zone support, handles daylight saving time correctly, and provides a clear distinction between instant, date, time, and zoned datetime types.

Temporal isn't in browsers yet, but you can use the @js-temporal/polyfill today in Node.js and modern bundlers. Here's how it changes the game:

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

// Create a specific instant in time (like UTC)
const instant = Temporal.Instant.from('2024-03-15T14:30:00Z');
console.log(instant.toString()); // 2024-03-15T14:30:00Z

// Create a zoned datetime—the same instant, different time zones
const tokyoTime = instant.toZonedDateTimeISO('Asia/Tokyo');
const nyTime = instant.toZonedDateTimeISO('America/New_York');

console.log(tokyoTime.toString()); // 2024-03-16T23:30:00+09:00[Asia/Tokyo]
console.log(nyTime.toString());    // 2024-03-15T10:30:00-04:00[America/New_York]

// Do arithmetic without worrying about DST
const meeting = Temporal.ZonedDateTime.from('2024-06-15T10:00:00[America/New_York]');
const oneWeekLater = meeting.add({ weeks: 1 });
console.log(oneWeekLater.toString()); // Handles DST transitions correctly

Temporal's immutability prevents entire classes of bugs. Once you create a datetime, it can't change:

typescript
const original = Temporal.ZonedDateTime.from('2024-03-15T14:30:00[UTC]');
const modified = original.add({ hours: 2 });
// original is still 2024-03-15T14:30:00[UTC]
// modified is 2024-03-15T16:30:00[UTC]

For teams building international applications, payment systems, or anything requiring reliable scheduling, Temporal is a game-changer. Start using the polyfill now—your future self will thank you when browser support lands.

Store UTC — Serialize as ISO strings, convert at the edges

Regardless of which API you use, follow this architectural principle: store everything in UTC, serialize as ISO 8601 strings, and convert to user time zones only at the edges—when displaying to users or accepting their input.

Store timestamps in your database as ISO strings or Unix timestamps in UTC:

typescript
// Good: Store as ISO 8601 UTC
const createdAt = '2024-03-15T14:30:00Z';

// Also good: Store as Unix timestamp (seconds or milliseconds)
const createdAtUnix = 1710511800;

// Avoid: Storing local times without zone info
// "2024-03-15 14:30:00" — where? Which year does this mean in Tokyo?

When you need to work with a time, convert it to UTC first, then to the user's zone only for display:

typescript
// Fetch from database
const storedTimestamp = '2024-03-15T14:30:00Z';

// Parse as UTC
const utcTime = new Date(storedTimestamp);

// Convert to user's time zone for display
const userTimeZone = Temporal.Now.zonedDateTimeISO().timeZoneId;
const formatter = new Intl.DateTimeFormat('en-US', {
  timeZone: userTimeZone,
  year: 'numeric',
  month: 'short',
  day: 'numeric',
  hour: '2-digit',
  minute: '2-digit',
});

console.log(formatter.format(utcTime)); // Displays in user's zone

When accepting user input, always ask for their time zone explicitly or use their browser's time zone:

typescript
// User submits: "March 15, 2024 at 2:30 PM" in their local time
function parseUserDateTime(dateString, userTimeZone) {
  // With Temporal polyfill
  const local = Temporal.PlainDateTime.from(dateString);
  const zoned = local.toZonedDateTime(userTimeZone);
  return zoned.toInstant(); // Convert to UTC for storage
}

// Usage
const userInput = '2024-03-15T14:30:00';
const userZone = 'America/New_York';
const utcTime = parseUserDateTime(userInput, userZone);
console.log(utcTime.toString()); // 2024-03-15T18:30:00Z

This pattern—UTC in storage, conversion at edges—scales across teams and services. A backend engineer doesn't need to think about time zones; a frontend engineer handles display; a mobile developer gets ISO strings and does the same conversion. Everyone works in UTC internally, reducing coordination overhead and bugs.

Common mistakes

  • Mixing local and UTC times without tracking which is which. Always label your timestamps: is this a UTC instant or a local wall-clock time? Use types or comments to make it clear.
  • Using Date for arithmetic across time zones. Never try to calculate "2 PM tomorrow" in a specific time zone using Date and manual offset math. DST transitions will catch you. Use Temporal or a robust library like date-fns.
  • Forgetting that months are zero-indexed. The new Date(year, month, day) constructor is a trap. Always use new Date('YYYY-MM-DD') or an ISO string instead.
  • Assuming your server and database are in the same time zone. Always explicitly set UTC. Many outages happen because someone deployed a service to a different region and forgot to adjust time zone assumptions.
  • Storing display-formatted times in the database. Never store "March 15, 2024" or "2:30 PM EST." Store UTC timestamps only, and format only on read.

Key takeaways

  • Date has real limitations—mutability, zero-indexed months, and weak time zone support—that make it dangerous for anything beyond trivial use cases. Understand its constraints and work around them deliberately.
  • Intl.DateTimeFormat and Intl.RelativeTimeFormat are built into modern JavaScript and handle localization and time zone display without dependencies. Use them for all user-facing output.
  • Temporal is arriving and solves nearly every pain point: immutability, proper time zone handling, DST correctness, and clear type distinctions. Start with the polyfill today if you work on complex scheduling or international features.
  • Architecture matters: store UTC in your database, serialize as ISO 8601 strings, and convert to user time zones only at the presentation layer. This separation makes systems easier to test, debug, and scale.
  • When in doubt, reach for a lightweight library like date-fns or day.js for arithmetic, or use Temporal if you're already using a bundler. Homegrown time zone math is almost always a source of bugs.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles