DEV Community

HAU
HAU

Posted on

Timezone Conversion in JavaScript: Why getTimezoneOffset() Will Betray You

Ask any JavaScript developer how to convert between timezones and someone will say: "use getTimezoneOffset()."

Don't.

Here's why, and what to use instead.

What getTimezoneOffset() Actually Returns

const d = new Date();
console.log(d.getTimezoneOffset()); // e.g., -330 for IST, 300 for US/Eastern
Enter fullscreen mode Exit fullscreen mode

Two problems with this:

1. It returns the local timezone offset, not an arbitrary one.

getTimezoneOffset() tells you the offset of the machine running the code. You cannot use it to find out what time it is in Tokyo from a server running in UTC.

2. The sign is backwards from what you'd expect.

UTC+5:30 (India) returns -330. UTC-5:00 (US East) returns 300. The value is UTC - local, not local - UTC. This trips up almost everyone the first time.

The Wrong Way to Convert Timezones

// ❌ This only works if the server happens to be in the right timezone
function toNewYorkTime(utcDate) {
  const offset = -300; // "NYC is UTC-5" hardcoded
  const localTime = new Date(utcDate.getTime() + offset * 60000);
  return localTime;
}
Enter fullscreen mode Exit fullscreen mode

This breaks the moment New York observes Daylight Saving Time (UTC-4, not -5). You've just hardcoded a bug that appears twice a year.

The Right Way: Intl.DateTimeFormat

Modern JavaScript has built-in timezone support via the Intl API:

// Display a UTC timestamp in any IANA timezone
function formatInTimezone(date, timezone, locale = 'en-US') {
  return new Intl.DateTimeFormat(locale, {
    timeZone: timezone,
    year: 'numeric',
    month: '2-digit',
    day: '2-digit',
    hour: '2-digit',
    minute: '2-digit',
    second: '2-digit',
    hour12: false,
  }).format(date);
}

const now = new Date();
formatInTimezone(now, 'America/New_York');   // "03/24/2026, 08:00:00"
formatInTimezone(now, 'Asia/Tokyo');         // "03/24/2026, 21:00:00"
formatInTimezone(now, 'Europe/Paris');       // "03/24/2026, 13:00:00"
Enter fullscreen mode Exit fullscreen mode

This correctly handles DST, historical timezone changes, and all the edge cases you'd never think of.

Getting the Offset for Any Timezone

Sometimes you need the numeric offset (for database storage, API calls, etc.):

function getTimezoneOffsetMinutes(timezone, date = new Date()) {
  // Format as offset string in the target timezone
  const formatter = new Intl.DateTimeFormat('en', {
    timeZone: timezone,
    timeZoneName: 'shortOffset', // 'GMT+5:30', 'GMT-4', etc.
  });

  const parts = formatter.formatToParts(date);
  const offsetStr = parts.find(p => p.type === 'timeZoneName')?.value;

  // Parse 'GMT+5:30' → +330, 'GMT-4' → -240
  const match = offsetStr?.match(/GMT([+-])(\d+)(?::(\d+))?/);
  if (!match) return 0;

  const sign = match[1] === '+' ? 1 : -1;
  const hours = parseInt(match[2], 10);
  const minutes = parseInt(match[3] || '0', 10);

  return sign * (hours * 60 + minutes);
}

getTimezoneOffsetMinutes('America/New_York');  // -240 (during EDT)
getTimezoneOffsetMinutes('Asia/Kolkata');      // +330 (IST, no DST)
Enter fullscreen mode Exit fullscreen mode

IANA Timezone Names Are What You Need

Forget abbreviations like EST, PST, IST. They're ambiguous:

  • IST = Indian Standard Time or Irish Standard Time or Israel Standard Time
  • CST = Central Standard Time (US) or China Standard Time or Cuba Standard Time

Always use IANA timezone database names: America/New_York, Asia/Kolkata, Pacific/Auckland. These are unambiguous, globally consistent, and maintained with DST rule updates.

// Get user's IANA timezone
const userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
console.log(userTimezone); // e.g., 'America/Los_Angeles'
Enter fullscreen mode Exit fullscreen mode

DST Edge Cases

Spring forward (clocks skip an hour):
In US/Eastern, 2:30am on March 8 doesn't exist. new Date('2026-03-08T02:30:00') in an ET environment will silently resolve to 3:30am.

Fall back (clocks repeat an hour):
1:30am on November 1 happens twice in US/Eastern. Without an explicit UTC offset, you can't distinguish them.

The only reliable way to handle these: store all timestamps in UTC, convert to local only for display.

// Store this
const event = { startsAt: '2026-11-01T06:30:00Z' }; // UTC ✓

// Display this
formatInTimezone(new Date(event.startsAt), 'America/New_York');
// Correctly shows 1:30 AM EST (after fall-back)
Enter fullscreen mode Exit fullscreen mode

The Temporal API (Coming Soon)

// Future Temporal API — clean timezone handling
const now = Temporal.Now.zonedDateTimeISO('America/New_York');
const tokyo = now.withTimeZone('Asia/Tokyo');
console.log(tokyo.toString()); // No ambiguity, no offset math
Enter fullscreen mode Exit fullscreen mode

Temporal makes timezone-aware arithmetic first-class. It's Stage 3 and shipping in Node 22+ experimentally.

Quick Timezone Lookup

When working across timezones, I often need a quick sanity check on what time it is somewhere right now. For static date conversions ("what time is 3pm EST in Tokyo?"), datetimecalculator.app handles the basics without needing to spin up a REPL.

Summary

  • getTimezoneOffset() only returns the local machine's offset, and the sign is inverted
  • Use Intl.DateTimeFormat with IANA timezone names for reliable display
  • Never hardcode UTC offsets — they change with DST
  • Always store in UTC; convert to local only at the presentation layer
  • Avoid timezone abbreviations; use full IANA names

What's the wildest timezone bug you've shipped? The DST "gains an hour" failures seem to be a universal developer experience.

Top comments (0)