Timezone handling is one of those things that looks simple until it isn't. Every developer has a story: the bug that only appeared twice a year, the timestamp that was off by an hour, the scheduled job that fired at the wrong time. I've had all of these, and I've helped dozens of teams fix them.
This guide covers the right way to handle timezones in JavaScript and Python — the two languages I use most in my 15 years of systems engineering work.
The Golden Rules
Before diving into code, here are the principles that prevent 90% of timezone bugs:
- Store timestamps in UTC — always, without exception
- Use IANA timezone identifiers (
America/New_York, notESTor-05:00) - Convert to local time only at display time — never in business logic
- Never do timezone math manually — use a library
JavaScript: The Right Way
The built-in: Intl.DateTimeFormat
Modern JavaScript has excellent built-in timezone support via the Intl API. You don't need moment.js for most use cases in 2026.
// Display a UTC timestamp in a specific timezone
const date = new Date('2026-03-15T14:30:00Z') // UTC
const formatter = new Intl.DateTimeFormat('en-US', {
timeZone: 'America/New_York',
year: 'numeric',
month: 'long',
day: 'numeric',
hour: 'numeric',
minute: '2-digit',
hour12: true,
})
console.log(formatter.format(date))
// "March 15, 2026 at 10:30 AM" (EST, UTC-4 in March)
Getting the current time in a timezone
// Get current time in Tokyo
const tokyoTime = new Date().toLocaleString('en-US', {
timeZone: 'Asia/Tokyo',
hour: 'numeric',
minute: '2-digit',
hour12: false,
})
console.log(tokyoTime) // e.g., "23:30"
Getting the UTC offset for a timezone
function getUtcOffsetMinutes(date, timeZone) {
const utcDate = new Date(date.toLocaleString('en-US', { timeZone: 'UTC' }))
const tzDate = new Date(date.toLocaleString('en-US', { timeZone }))
return (tzDate - utcDate) / 60000
}
console.log(getUtcOffsetMinutes(new Date(), 'Asia/Kolkata')) // 330 (UTC+5:30)
console.log(getUtcOffsetMinutes(new Date(), 'America/New_York')) // -240 or -300
When to use a library
For complex operations — parsing timezone-aware strings, calculating business hours, handling recurring events — use date-fns-tz (which QuickTZone itself uses):
import { formatInTimeZone, toZonedTime } from 'date-fns-tz'
const date = new Date('2026-03-15T14:30:00Z')
const formatted = formatInTimeZone(date, 'Europe/London', 'yyyy-MM-dd HH:mm zzz')
console.log(formatted) // "2026-03-15 14:30 GMT"
Avoid: moment.js (deprecated, large bundle), raw offset arithmetic, storing local times in databases.
Python: The Right Way
The built-in: zoneinfo (Python 3.9+)
Python 3.9 introduced zoneinfo, which replaces pytz for most use cases:
from datetime import datetime
from zoneinfo import ZoneInfo
# Create a UTC datetime
utc_time = datetime(2026, 3, 15, 14, 30, 0, tzinfo=ZoneInfo('UTC'))
# Convert to New York time
ny_time = utc_time.astimezone(ZoneInfo('America/New_York'))
print(ny_time) # 2026-03-15 10:30:00-04:00
# Convert to Tokyo time
tokyo_time = utc_time.astimezone(ZoneInfo('Asia/Tokyo'))
print(tokyo_time) # 2026-03-16 23:30:00+09:00
Getting the current time in a timezone
from datetime import datetime
from zoneinfo import ZoneInfo
# Current time in Mumbai
mumbai_now = datetime.now(ZoneInfo('Asia/Kolkata'))
print(mumbai_now.strftime('%H:%M %Z')) # e.g., "20:00 IST"
Storing and retrieving from a database
from datetime import datetime, timezone
from zoneinfo import ZoneInfo
# Always store in UTC
def store_event(event_time_local: datetime, user_tz: str) -> datetime:
"""Convert local time to UTC for storage."""
if event_time_local.tzinfo is None:
# Assume the naive datetime is in the user's timezone
aware = event_time_local.replace(tzinfo=ZoneInfo(user_tz))
else:
aware = event_time_local
return aware.astimezone(timezone.utc)
# Always display in user's timezone
def display_event(utc_time: datetime, user_tz: str) -> str:
local_time = utc_time.astimezone(ZoneInfo(user_tz))
return local_time.strftime('%B %d, %Y at %I:%M %p %Z')
Using pytz (Python < 3.9)
If you're on Python 3.8 or earlier, use pytz:
import pytz
from datetime import datetime
utc_time = datetime(2026, 3, 15, 14, 30, 0, tzinfo=pytz.UTC)
ny_tz = pytz.timezone('America/New_York')
ny_time = utc_time.astimezone(ny_tz)
print(ny_time) # 2026-03-15 10:30:00-04:00
Important: With pytz, always use localize() for naive datetimes, never replace():
# WRONG - can give incorrect results during DST transitions
wrong = naive_dt.replace(tzinfo=ny_tz)
# CORRECT
correct = ny_tz.localize(naive_dt)
Common Mistakes to Avoid
| Mistake | Problem | Fix |
|---|---|---|
| Storing local time in DB | DST transitions corrupt data | Store UTC always |
Using EST instead of America/New_York |
Ambiguous, DST-unaware | Use IANA identifiers |
| Manual offset arithmetic | Breaks at DST boundaries | Use a library |
new Date('2026-03-15') in JS |
Parsed as UTC midnight, not local | Use explicit timezone |
datetime.now() without tzinfo |
Returns naive datetime | Use datetime.now(timezone.utc) |
Testing Timezone Code
Always test your timezone code around DST transition dates. In 2026:
- US spring forward: March 8
- US fall back: November 1
- EU spring forward: March 29
- EU fall back: October 25
Write tests that explicitly use these dates and verify your code handles the transitions correctly.
Manual QA Checklist for Timezone Features
When you ship a timezone-sensitive feature, test it with real city pairs rather than only offsets:
| Scenario | Why to test it |
|---|---|
| New York to London | DST starts on different dates in the US and UK |
| San Francisco to Berlin | Large offset and European DST rules |
| Mumbai to Dubai | Half-hour offset next to a whole-hour offset |
| Tokyo to Sydney | One city does not observe DST while the other can |
QuickTZone is useful here even for developers: enter the same cities your users care about, move the planner date across DST boundaries, and compare the UI output with your code's output.
Author
Written by a systems engineer with 15 years of experience in distributed systems, backend development, and international software architecture.