// Get timezone info for America/New_York
const response = await fetch(
"https://worlddataapi.com/v1/timezones/America%2FNew_York",
{ headers: { "X-API-Key": "YOUR_API_KEY" } },
);
const data = await response.json();
console.log(data);
// {
// "id": "America/New_York",
// "name": "New York",
// "country": "US",
// "offset": "-05:00",
// "offset_minutes": -300
// }
import requests
response = requests.get(
"https://worlddataapi.com/v1/timezones/America%2FNew_York",
headers={"X-API-Key": "YOUR_API_KEY"}
)
data = response.json()
print(data)
# {
# "id": "America/New_York",
# "name": "New York",
# "country": "US",
# "offset": "-05:00",
# "offset_minutes": -300
# }
curl -X GET "https://worlddataapi.com/v1/timezones/America%2FNew_York" \
-H "X-API-Key: YOUR_API_KEY"
Timezone bugs are subtle and embarrassing. A meeting scheduled for 3pm shows up at 3am. A user in Tokyo sees yesterday's date. A DST transition duplicates records. This guide covers the principles and practices that prevent these bugs.
The Challenge#
Building timezone-aware applications is harder than it appears. The core difficulties include:
Ambiguous local times. Without a timezone, "3:00 PM" is meaningless. And timezone abbreviations like "EST" are ambiguous (multiple countries use the same abbreviations).
DST transitions. On "spring forward" day, some times don't exist. On "fall back" day, some times occur twice.
Changing rules. Countries modify their UTC offsets and DST policies. Russia abolished DST in 2011. Turkey changed its rules in 2016. Your hardcoded assumptions will break.
Non-standard offsets. Not all timezones use whole-hour offsets. India is UTC+5:30. Nepal is UTC+5:45.
The solution is a disciplined approach: store all timestamps in UTC, use IANA timezone identifiers, and convert to local time only at display time.
Prerequisites#
A World Data API key (free tier available at worlddataapi.com)
Basic knowledge of JavaScript or Python
Familiarity with ISO 8601 date format (
2026-01-15T18:00:00.000Z)A database that supports timestamp storage (PostgreSQL, MySQL, MongoDB, etc.)
The Golden Rule#
Store UTC. Display local.
Every timestamp in your database should be UTC. Convert to local time only at the moment of display, using the viewer's timezone.
// Storing an event
const event = {
title: "Team Meeting",
// Store as UTC
scheduledAt: new Date().toISOString(), // "2026-01-15T18:00:00.000Z"
// Store the intended timezone separately
timezone: "America/New_York",
};
// Displaying the event
function displayEvent(event, viewerTimezone) {
const date = new Date(event.scheduledAt);
return {
title: event.title,
displayTime: date.toLocaleString("en-US", {
timeZone: viewerTimezone,
dateStyle: "medium",
timeStyle: "short",
}),
};
}
Why Not Store Local Time?#
Local time is ambiguous:
"3:00 PM" where? Without a timezone, the time is meaningless.
DST transitions create duplicates. On "fall back" day, 1:30 AM happens twice.
DST transitions create gaps. On "spring forward" day, 2:30 AM doesn't exist.
Timezones change. Countries modify their UTC offsets and DST rules.
UTC has none of these problems. It's unambiguous, continuous, and stable.
IANA Timezone Identifiers#
Always use IANA timezone identifiers (America/New_York), never abbreviations (EST):
// DON'T DO THIS
const timezone = "EST"; // Ambiguous and doesn't handle DST
// DO THIS
const timezone = "America/New_York"; // Unambiguous, DST-aware
Why abbreviations fail:
CSTcould be Central Standard Time (US), China Standard Time, or Cuba Standard TimeESTdoesn't account for Eastern Daylight TimeAbbreviations aren't standardized
World Data API provides IANA identifiers for all 316 timezones:
// List all timezones
const response = await fetch("https://worlddataapi.com/v1/timezones", {
headers: { "X-API-Key": API_KEY },
});
const data = await response.json();
// Returns paginated list of IANA timezone objects
Basic Timezone Operations#
Getting User's Timezone#
// Browser API
const userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
// "America/New_York"
Converting UTC to Local#
function formatInTimezone(utcDate, timezone, locale = "en-US") {
return new Date(utcDate).toLocaleString(locale, {
timeZone: timezone,
year: "numeric",
month: "short",
day: "numeric",
hour: "numeric",
minute: "2-digit",
timeZoneName: "short",
});
}
// UTC timestamp
const utc = "2026-01-15T18:00:00.000Z";
// Display in different timezones
console.log(formatInTimezone(utc, "America/New_York"));
// "Jan 15, 2026, 1:00 PM EST"
console.log(formatInTimezone(utc, "Europe/London"));
// "Jan 15, 2026, 6:00 PM GMT"
console.log(formatInTimezone(utc, "Asia/Tokyo"));
// "Jan 16, 2026, 3:00 AM JST"
Converting Local to UTC#
When a user enters a time, convert to UTC for storage:
function localToUTC(dateString, timeString, timezone) {
// Create a date string that JavaScript will parse in the given timezone
const localDateTime = `${dateString}T${timeString}:00`;
// Use Intl to get the offset for this timezone at this time
const formatter = new Intl.DateTimeFormat("en-US", {
timeZone: timezone,
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: false,
});
// This is a simplified approach - production code should use a library
// like Luxon or date-fns-tz for accurate conversion
const date = new Date(localDateTime);
// Get offset in minutes
const utcDate = new Date(date.toLocaleString("en-US", { timeZone: "UTC" }));
const tzDate = new Date(
date.toLocaleString("en-US", { timeZone: timezone }),
);
const offset = (utcDate - tzDate) / 60000;
// Apply offset
const result = new Date(date.getTime() - offset * 60000);
return result.toISOString();
}
// User in New York enters 1:00 PM on Jan 15
const utc = localToUTC("2026-01-15", "13:00", "America/New_York");
// "2026-01-15T18:00:00.000Z"
For production applications, use a library like Luxon or date-fns-tz for reliable conversions.
Building a Timezone Selector#
function TimezoneSelector({ value, onChange }) {
const [timezones, setTimezones] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function fetchAllTimezones() {
const allTimezones = [];
let page = 1;
let hasMore = true;
while (hasMore) {
const response = await fetch(
`https://worlddataapi.com/v1/timezones?per_page=100&page=${page}`,
{ headers: { "X-API-Key": API_KEY } },
);
const data = await response.json();
allTimezones.push(...data.data);
// Check if there are more pages (316 timezones total)
hasMore = data.data.length === 100;
page++;
}
setTimezones(allTimezones);
setLoading(false);
}
fetchAllTimezones();
}, []);
if (loading)
return (
<select disabled>
<option>Loading...</option>
</select>
);
// Group by region
const grouped = timezones.reduce((acc, tz) => {
const region = tz.id.split("/")[0];
if (!acc[region]) acc[region] = [];
acc[region].push(tz);
return acc;
}, {});
return (
<select value={value} onChange={(e) => onChange(e.target.value)}>
<option value="">Select timezone...</option>
{Object.entries(grouped).map(([region, zones]) => (
<optgroup key={region} label={region}>
{zones.map((tz) => (
<option key={tz.id} value={tz.id}>
{tz.name} ({tz.offset})
</option>
))}
</optgroup>
))}
</select>
);
}
Common Timezones First#
Most users are in a handful of timezones. Put common ones at the top:
const COMMON_TIMEZONES = [
"America/New_York",
"America/Chicago",
"America/Denver",
"America/Los_Angeles",
"Europe/London",
"Europe/Paris",
"Asia/Tokyo",
"Asia/Shanghai",
"Australia/Sydney",
];
function TimezoneSelector({ value, onChange, timezones }) {
const common = COMMON_TIMEZONES.map((id) =>
timezones.find((tz) => tz.id === id),
).filter(Boolean);
const others = timezones.filter((tz) => !COMMON_TIMEZONES.includes(tz.id));
return (
<select value={value} onChange={(e) => onChange(e.target.value)}>
<optgroup label="Common Timezones">
{common.map((tz) => (
<option key={tz.id} value={tz.id}>
{tz.name} ({tz.offset})
</option>
))}
</optgroup>
<optgroup label="All Timezones">
{others.map((tz) => (
<option key={tz.id} value={tz.id}>
{tz.id} ({tz.offset})
</option>
))}
</optgroup>
</select>
);
}
Handling DST Transitions#
DST transitions create two types of edge cases:
The Gap (Spring Forward)#
When clocks spring forward, times in the gap don't exist:
// On March 8, 2026 at 2:00 AM, clocks jump to 3:00 AM
// 2:30 AM doesn't exist
function isValidTime(dateString, timeString, timezone) {
// Convert to UTC and back
const utc = localToUTC(dateString, timeString, timezone);
const roundTrip = formatInTimezone(utc, timezone);
// If the time changed, the original was in the gap
const originalTime = `${dateString} ${timeString}`;
return roundTrip.includes(timeString);
}
The Overlap (Fall Back)#
When clocks fall back, times in the overlap occur twice:
// On November 1, 2026 at 2:00 AM, clocks fall back to 1:00 AM
// 1:30 AM happens twice
// When storing times in this range, you need to capture which occurrence
const event = {
localTime: "01:30",
timezone: "America/New_York",
// Store the UTC time to disambiguate
utc: "2026-11-01T05:30:00.000Z", // First occurrence
// or
// utc: '2026-11-01T06:30:00.000Z' // Second occurrence
};
Recurring Events#
Recurring events across timezones are especially tricky:
// "Every Monday at 3pm New York time"
const recurringEvent = {
rule: "RRULE:FREQ=WEEKLY;BYDAY=MO",
time: "15:00",
timezone: "America/New_York",
};
// When generating occurrences, calculate each one individually
function getNextOccurrence(event, after = new Date()) {
// Don't just add 7 days to the UTC time!
// Calculate in local time, then convert to UTC
// Find next Monday in the event's timezone
const nextMonday = findNextMonday(after, event.timezone);
// Combine with the event time
const localDateTime = `${nextMonday}T${event.time}:00`;
// Convert to UTC
return localToUTC(localDateTime.split("T")[0], event.time, event.timezone);
}
The key insight: recurring events should be stored as "3pm New York time", not as a UTC time that you add intervals to. Adding 7 days to a UTC timestamp breaks when DST transitions occur.
Database Patterns#
PostgreSQL#
-- Store timestamps as TIMESTAMPTZ (timestamp with time zone)
CREATE TABLE events (
id SERIAL PRIMARY KEY,
title TEXT NOT NULL,
scheduled_at TIMESTAMPTZ NOT NULL, -- Stored as UTC
timezone TEXT NOT NULL -- User's intended timezone
);
-- Insert with explicit timezone
INSERT INTO events (title, scheduled_at, timezone)
VALUES (
'Team Meeting',
'2026-01-15 13:00:00 America/New_York',
'America/New_York'
);
-- Query with timezone conversion
SELECT
title,
scheduled_at AT TIME ZONE 'America/New_York' as local_time
FROM events;
MongoDB#
// Store as Date (always UTC) with timezone metadata
const event = {
title: "Team Meeting",
scheduledAt: new Date("2026-01-15T18:00:00.000Z"),
timezone: "America/New_York",
// Optionally store original local time for reference
originalLocalTime: {
date: "2026-01-15",
time: "13:00",
},
};
Common Pitfalls#
Pitfall 1: Using Offsets Instead of Timezones#
// DON'T
const event = { time: "2026-01-15T13:00:00-05:00" };
// The -05:00 offset is correct for January, but wrong for July (DST)
// DO
const event = {
time: "2026-01-15T18:00:00.000Z", // UTC
timezone: "America/New_York", // IANA identifier handles DST
};
Pitfall 2: Assuming All Days Have 24 Hours#
// DON'T
function addOneDay(date) {
return new Date(date.getTime() + 24 * 60 * 60 * 1000);
}
// DO
function addOneDay(date, timezone) {
// Work with calendar dates, not milliseconds
const options = { timeZone: timezone };
const localDate = new Date(date.toLocaleString("en-US", options));
localDate.setDate(localDate.getDate() + 1);
return localDate;
}
On DST transition days, the local day has 23 or 25 hours.
Pitfall 3: Parsing Dates Without Timezone Context#
// DON'T
const date = new Date("2026-01-15 13:00:00");
// Parsed in local timezone of the server/browser — inconsistent!
// DO
const date = new Date("2026-01-15T13:00:00-05:00");
// Or better:
const date = new Date("2026-01-15T18:00:00.000Z");
Pitfall 4: Displaying Times Without Timezone Indicator#
// DON'T
"Meeting at 3:00 PM";
// 3:00 PM where?
// DO
"Meeting at 3:00 PM EST";
// or
"Meeting at 3:00 PM (New York time)";
// or show in viewer's timezone:
"Meeting at 9:00 PM your time (3:00 PM EST)";
Testing Timezone Code#
describe("Timezone handling", () => {
it("handles DST spring forward gap", () => {
// 2:30 AM on March 8, 2026 doesn't exist in New York
const result = localToUTC("2026-03-08", "02:30", "America/New_York");
// Should handle gracefully — typically resolve to 3:30 AM
});
it("handles DST fall back overlap", () => {
// 1:30 AM on November 1, 2026 happens twice in New York
// Your code should have a way to specify which occurrence
});
it("maintains consistency across timezones", () => {
const utc = "2026-01-15T18:00:00.000Z";
const utcTimestamp = new Date(utc).getTime();
// Verify the UTC timestamp is preserved regardless of display timezone
// The original UTC string should always parse to the same timestamp
expect(new Date(utc).getTime()).toBe(utcTimestamp);
// Different displays should show different local times but same instant
const ny = formatInTimezone(utc, "America/New_York");
const tokyo = formatInTimezone(utc, "Asia/Tokyo");
// NY shows 1:00 PM, Tokyo shows 3:00 AM next day - both are same instant
expect(ny).toContain("1:00 PM");
expect(tokyo).toContain("3:00 AM");
});
it("handles non-standard offsets", () => {
// Nepal is UTC+5:45
const nepal = formatInTimezone(
"2026-01-15T12:00:00.000Z",
"Asia/Kathmandu",
);
expect(nepal).toContain("5:45 PM");
});
});
Recommended Libraries#
For production applications, use a timezone library:
Luxon — Modern, immutable, comprehensive
date-fns-tz — Modular, tree-shakeable
Temporal — The future standard (Stage 3 proposal)
// Luxon example
import { DateTime } from "luxon";
const dt = DateTime.fromISO("2026-01-15T18:00:00.000Z");
const nyTime = dt.setZone("America/New_York");
console.log(nyTime.toFormat("MMM d, yyyy h:mm a ZZZZ"));
// "Jan 15, 2026 1:00 PM Eastern Standard Time"
Summary#
Building timezone-aware applications requires discipline around a few core principles:
Store UTC. All timestamps in your database should be in UTC. This eliminates ambiguity and ensures consistency.
Use IANA identifiers. Always use
America/New_York, neverEST. IANA identifiers handle DST transitions automatically.Display local. Convert to the user's timezone only at the moment of display.
Test edge cases. DST transitions create gaps and overlaps. Non-standard offsets (like Nepal's UTC+5:45) break assumptions.
Use libraries. For production code, use Luxon, date-fns-tz, or the upcoming Temporal API instead of rolling your own conversions.
World Data API provides timezone metadata for all 316 IANA timezones, making it straightforward to build timezone selectors, validate user input, and display timezone information in your application.
Ready to build timezone-aware applications? Get your free API key and start integrating timezone data today. The free tier includes 60 requests per day with access to all timezone endpoints.
Next Steps#
Building a Meeting Scheduler Across Timezones - Practical application
Working with ISO Country and Region Codes - Location data
Caching Strategies for Reference Data APIs - Optimize API usage