| name | javascript-utc-date-timezone-drift |
| description | Fix timezone drift when using JavaScript Date methods with UTC-stored times. Use when: (1) Times shift by user's timezone offset (e.g., 09:00 UTC becomes 10:00 in BST browsers), (2) Database stores times in UTC but display shows wrong hours, (3) Inconsistent time behavior across users in different timezones, (4) Using getHours/getMinutes/setHours with Date objects that should stay in UTC. Replace local time getters/setters with UTC variants to prevent automatic timezone conversion.
|
| author | Claude Code |
| version | 1.0.0 |
| date | "2026-02-06T00:00:00.000Z" |
JavaScript UTC Date Timezone Drift Fix
Problem
When working with JavaScript Date objects that represent UTC times (e.g., from a database or API), using local time methods like getHours(), getMinutes(), or setHours() causes timezone drift. The browser automatically applies the user's local timezone offset, causing times to shift unexpectedly.
This creates silent bugs where:
- 09:00 UTC appears as 10:00 for users in BST (UTC+1)
- 09:00 UTC appears as 01:00 for users in PST (UTC-8)
- Times work correctly for developers in UTC but fail in production for global users
Context / Trigger Conditions
Use this skill when you see:
-
Time shift by timezone offset: A time stored as 09:00 UTC displays as 10:00, 01:00, or another value depending on user location
-
Inconsistent behavior across timezones: Times appear correct for some users but wrong for others
-
Database times don't match display: Your database stores 2026-01-01T09:00:00Z but the UI shows different hours
-
Working with UTC-stored times: You're using Date objects created from ISO 8601 strings with Z suffix or from database timestamps stored in UTC
-
Code uses local getters/setters:
date.getHours()
date.setHours(h)
date.getMinutes()
date.setMinutes(m)
Error symptoms:
- Times appear shifted by 1-14 hours depending on user location
- No JavaScript errors thrown (silent bug)
- Works correctly in development (UTC timezone) but fails in production
- Different behavior for users in different countries
Solution
Step 1: Identify UTC Date Objects
First, confirm you're working with UTC times:
const utcDate = new Date("2026-01-01T09:00:00Z");
const fromDB = new Date(dbTimestamp);
const fromAPI = new Date(apiResponse.startTime);
const localDate = new Date(2026, 0, 1, 9, 0);
const userInput = new Date(datePickerValue);
Step 2: Replace Local Methods with UTC Variants
Reading time components:
const hours = date.getHours();
const minutes = date.getMinutes();
const hours = date.getUTCHours();
const minutes = date.getUTCMinutes();
Setting time components:
const newDate = new Date(date);
newDate.setHours(hours, minutes);
const newDate = new Date(date);
newDate.setUTCHours(hours, minutes);
Step 3: Complete Method Mapping
| ❌ Local Method | ✅ UTC Equivalent | Purpose |
|---|
getHours() | getUTCHours() | Get hour (0-23) |
getMinutes() | getUTCMinutes() | Get minute (0-59) |
getSeconds() | getUTCSeconds() | Get second (0-59) |
getMilliseconds() | getUTCMilliseconds() | Get millisecond (0-999) |
getDay() | getUTCDay() | Get day of week (0-6) |
getDate() | getUTCDate() | Get day of month (1-31) |
getMonth() | getUTCMonth() | Get month (0-11) |
getFullYear() | getUTCFullYear() | Get year |
setHours(h, m, s, ms) | setUTCHours(h, m, s, ms) | Set time components |
setMinutes(m, s, ms) | setUTCMinutes(m, s, ms) | Set minute and smaller |
setSeconds(s, ms) | setUTCSeconds(s, ms) | Set second and smaller |
setDate(d) | setUTCDate(d) | Set day of month |
setMonth(m, d) | setUTCMonth(m, d) | Set month and day |
setFullYear(y, m, d) | setUTCFullYear(y, m, d) | Set year, month, day |
Step 4: Real-World Example
Before (buggy code with timezone drift):
function expandAvailability(availability, startDate, endDate) {
const instances = [];
let currentDate = new Date(startDate);
while (currentDate <= endDate) {
if (currentDate.getDay() === availability.dayOfWeek) {
const instanceStart = new Date(currentDate);
instanceStart.setHours(
availability.startTime.getHours(),
availability.startTime.getMinutes()
);
instances.push(instanceStart);
}
currentDate.setDate(currentDate.getDate() + 1);
}
return instances;
}
After (fixed with UTC methods):
function expandAvailability(availability, startDate, endDate) {
const instances = [];
let currentDate = new Date(startDate);
while (currentDate <= endDate) {
if (currentDate.getUTCDay() === availability.dayOfWeek) {
const instanceStart = new Date(currentDate);
instanceStart.setUTCHours(
availability.startTime.getUTCHours(),
availability.startTime.getUTCMinutes()
);
instances.push(instanceStart);
}
currentDate.setUTCDate(currentDate.getUTCDate() + 1);
}
return instances;
}
Verification
After applying the fix:
- ✅ Test in multiple timezones: Change your system timezone and verify times don't shift
- ✅ Check database roundtrip: Save a time, reload, verify it matches
- ✅ Compare UTC strings: Use
.toISOString() to verify times are identical
- ✅ Test edge cases: Midnight (00:00), noon (12:00), end of day (23:59)
Test script:
const utcTime = new Date("2026-01-01T09:00:00Z");
console.log("Original UTC time:", utcTime.toISOString());
const localHours = utcTime.getHours();
console.log("Local getHours():", localHours);
const utcHours = utcTime.getUTCHours();
console.log("UTC getUTCHours():", utcHours);
const buggyDate = new Date("2026-02-01");
buggyDate.setHours(utcTime.getHours(), utcTime.getMinutes());
console.log("Buggy reconstruction:", buggyDate.toISOString());
const fixedDate = new Date("2026-02-01");
fixedDate.setUTCHours(utcTime.getUTCHours(), utcTime.getUTCMinutes());
console.log("Fixed reconstruction:", fixedDate.toISOString());
Example: Complete Pattern
interface AvailabilityBlock {
id: string;
dayOfWeek: number;
startTime: Date;
endTime: Date;
effectiveFrom: Date;
effectiveUntil: Date | null;
}
function generateInstances(
availability: AvailabilityBlock,
startDate: Date,
endDate: Date
): Array<{ start: Date; end: Date }> {
const instances = [];
let currentDate = new Date(startDate);
const effectiveEnd = availability.effectiveUntil || endDate;
while (currentDate <= endDate && currentDate <= effectiveEnd) {
if (
currentDate.getUTCDay() === availability.dayOfWeek &&
currentDate >= availability.effectiveFrom
) {
const instanceStart = new Date(currentDate);
instanceStart.setUTCHours(
availability.startTime.getUTCHours(),
availability.startTime.getUTCMinutes(),
0,
0
);
const instanceEnd = new Date(currentDate);
instanceEnd.setUTCHours(
availability.endTime.getUTCHours(),
availability.endTime.getUTCMinutes(),
0,
0
);
instances.push({
start: instanceStart,
end: instanceEnd,
});
}
currentDate.setUTCDate(currentDate.getUTCDate() + 1);
}
return instances;
}
Notes
When to Use Local Methods
Local methods are appropriate when working with user-local times:
const userInput = datePicker.value;
const hours = userInput.getHours();
When to Use UTC Methods
UTC methods are required when working with stored UTC times:
const dbTime = new Date(dbRow.start_time);
const hours = dbTime.getUTCHours();
Timezone Library Alternatives
For complex timezone handling, consider libraries:
- date-fns-tz: Timezone-aware date utilities
- luxon: Immutable date library with timezone support
- dayjs with timezone plugin: Lightweight alternative
However, for simple UTC time preservation, native UTC methods are sufficient and preferred (no dependencies, better performance).
Common Pitfall: Date Constructor
The Date() constructor itself can cause confusion:
new Date(2026, 0, 1, 9, 0)
new Date("2026-01-01T09:00:00Z")
new Date(Date.UTC(2026, 0, 1, 9, 0))
TypeScript Type Safety
Consider creating wrapper types to enforce UTC handling:
type UTCDate = Date & { __brand: "UTC" };
function toUTC(date: Date): UTCDate {
return date as UTCDate;
}
function getUTCHoursSafe(date: UTCDate): number {
return date.getUTCHours();
}
References