The Hidden Complexity of Date Handling in Modern Apps
A practical guide to handling dates, times, and timezones in modern applications. Stop the datetime headaches.
Everyone thinks they know how to handle dates in their applications. Until they need to deal with timezones, DST changes, or calendar rules. Here's what you need to know.
The Common Pitfalls
1. Naive DateTime Storage
// ❌ BAD: Storing dates without context
interface Meeting {
id: string;
title: string;
startTime: Date; // Which timezone?
endTime: Date; // DST problems waiting to happen
}
// ❌ BAD: Timestamp assumptions
const meeting = {
startTime: new Date('2024-03-10T02:30:00'), // DST change in US
endTime: new Date('2024-03-10T03:30:00')
};
2. Timezone Confusion
// ❌ BAD: Implicit timezone conversion
function scheduleInterview(dateStr: string) {
const interviewTime = new Date(dateStr);
// Is this UTC? Local? User's timezone?
return db.interviews.create({
scheduledFor: interviewTime
});
}
// ❌ BAD: Mixing timezone handling
const local = new Date();
const utc = new Date(Date.UTC(
local.getFullYear(),
local.getMonth(),
local.getDate()
)); // Still problematic
Better Approaches
1. Explicit Timezone Handling
import { DateTime } from 'luxon';
interface Meeting {
id: string;
title: string;
startTime: string; // ISO string
timeZone: string; // e.g., 'America/New_York'
durationMinutes: number; // Store duration, not end time
}
class MeetingService {
schedule(meeting: CreateMeetingDTO): Meeting {
const start = DateTime.fromISO(meeting.startTime, {
zone: meeting.timeZone
});
// Validate business hours in correct timezone
if (!this.isBusinessHour(start)) {
throw new Error('Outside business hours');
}
return {
...meeting,
startTime: start.toUTC().toISO(),
endTime: start
.plus({ minutes: meeting.durationMinutes })
.toUTC()
.toISO()
};
}
private isBusinessHour(time: DateTime): boolean {
const hour = time.hour;
return hour >= 9 && hour < 17;
}
}
2. Smart Display Components
// React component handling timezone display
interface TimeDisplayProps {
isoString: string;
userTimeZone: string;
format?: string;
}
function TimeDisplay({
isoString,
userTimeZone,
format = 'ff'
}: TimeDisplayProps) {
const time = DateTime.fromISO(isoString).setZone(userTimeZone);
return (
<time dateTime={isoString}>
{time.toFormat(format)}
<span className="text-gray-500 text-sm">
({time.zoneName})
</span>
</time>
);
}
// Usage
<TimeDisplay
isoString="2024-03-15T14:30:00Z"
userTimeZone="America/New_York"
format="fff"
/>
3. Recurring Events
class RecurringMeeting {
constructor(
private readonly rule: string, // RRULE format
private readonly baseTime: DateTime,
private readonly timeZone: string
) {}
getNextOccurrences(count: number): DateTime[] {
const rrule = rrulestr(this.rule, {
dtstart: this.baseTime.toJSDate()
});
return rrule.all((date, i) => i < count)
.map(date =>
DateTime.fromJSDate(date)
.setZone(this.timeZone)
);
}
isValidOccurrence(date: DateTime): boolean {
// Check if date matches rule
const rrule = rrulestr(this.rule, {
dtstart: this.baseTime.toJSDate()
});
return rrule.between(
date.minus({ minutes: 1 }).toJSDate(),
date.plus({ minutes: 1 }).toJSDate()
).length > 0;
}
}
Best Practices
1. Storage
interface DateTimeEntity {
timestamp: string; // ISO 8601 in UTC
timezone: string; // IANA timezone
originalTime: string; // Local time as entered by user
}
class DateTimeStorage {
store(input: DateTimeInput): DateTimeEntity {
const dt = DateTime.fromISO(input.time, {
zone: input.timezone
});
return {
timestamp: dt.toUTC().toISO(),
timezone: input.timezone,
originalTime: input.time
};
}
}
2. API Responses
interface TimeResponse {
iso: string; // For machine consumption
timezone: string; // User's timezone
display: {
local: string; // Formatted in user's timezone
original: string; // As entered
utc: string; // UTC time
};
}
class TimeResponseBuilder {
build(entity: DateTimeEntity, userTz: string): TimeResponse {
const time = DateTime.fromISO(entity.timestamp)
.setZone(userTz);
return {
iso: entity.timestamp,
timezone: userTz,
display: {
local: time.toFormat('ff'),
original: entity.originalTime,
utc: time.toUTC().toFormat('ff')
}
};
}
}
Key Takeaways
- Always Store:
- UTC timestamps
- Original timezone
- Original local time
- Never Assume:
- User's timezone
- DST rules
- Calendar rules
- Use Good Tools:
- Luxon/date-fns-tz for modern apps
- Moment-timezone for legacy
- IANA timezone database
Remember: Date handling is complex because time itself is complex. Don't reinvent the wheel - use proven libraries and patterns.