๐
JavaScript Time Handling Guide
Master modern JavaScript date handling, from native Date API limitations to Luxon best practices for robust time operations.
โ
Use
- โข
Luxon
for complex date operations - โข
Intl.DateTimeFormat
for formatting - โข
Date.toISOString()
for serialization - โข
Temporal
API (when available) - โข UTC for storage and calculations
โ
Avoid
- โข
new Date()
string parsing - โข
Date.parse()
for user input - โข Local time for storage
- โข
moment.js
(deprecated) - โข Timezone-naive arithmetic
โ ๏ธ
Native Date API Limitations
โ Common Date API Pitfalls
// โ Dangerous: Inconsistent string parsing
new Date("2024-03-10"); // Parsed as UTC midnight
new Date("2024/03/10"); // Parsed as local midnight
new Date("March 10, 2024"); // Parsed as local midnight
// Same input, different interpretations!
// โ Dangerous: Month is zero-indexed (but day isn't!)
new Date(2024, 2, 10); // March 10th (month 2 = March!)
new Date(2024, 12, 10); // January 10th, 2025 (wraps around!)
// โ Dangerous: Timezone-naive arithmetic
const start = new Date("2024-03-10T01:30:00"); // What timezone?
const end = new Date(start.getTime() + 2 * 60 * 60 * 1000); // +2 hours
// May skip or duplicate hours during DST!
// โ Dangerous: getYear() returns year - 1900
const date = new Date();
console.log(date.getYear()); // 124 (not 2024!)
// โ Dangerous: Mutation
const date1 = new Date("2024-03-10");
const date2 = date1;
date2.setDate(15); // Mutates both date1 and date2!
โ Safer Native Date Usage
// โ
Safe: Use ISO 8601 strings for parsing
const utcDate = new Date("2024-03-10T14:30:00.000Z");
const localDate = new Date("2024-03-10T14:30:00-05:00");
// โ
Safe: Use getFullYear() instead of getYear()
const year = new Date().getFullYear(); // 2024
// โ
Safe: Be explicit about timezone for display
const date = new Date();
console.log(date.toISOString()); // UTC: 2024-03-10T19:30:00.000Z
console.log(date.toLocaleString()); // Local: 3/10/2024, 2:30:00 PM
// โ
Safe: Work in UTC for calculations
const utcStart = new Date("2024-03-10T06:30:00.000Z"); // 1:30 AM EST
const utcEnd = new Date(utcStart.getTime() + 2 * 60 * 60 * 1000);
console.log(utcEnd.toISOString()); // 2024-03-10T08:30:00.000Z
// โ
Safe: Create new instances instead of mutating
const date1 = new Date("2024-03-10");
const date2 = new Date(date1.getTime());
date2.setDate(15); // Only date2 is modified
๐
Modern JavaScript Date Handling
Luxon Library (Recommended)
import { DateTime } from 'luxon';
// โ
Safe: Explicit timezone handling
const utcNow = DateTime.utc();
const localNow = DateTime.local();
const nyNow = DateTime.now().setZone('America/New_York');
// โ
Safe: Immutable operations
const start = DateTime.fromISO('2024-03-10T14:30:00', { zone: 'America/New_York' });
const end = start.plus({ hours: 2 }); // Returns new instance
console.log(start.toISO()); // Original unchanged
// โ
Safe: Timezone conversion
const utcTime = DateTime.utc(2024, 3, 10, 19, 30);
const nyTime = utcTime.setZone('America/New_York');
const tokyoTime = utcTime.setZone('Asia/Tokyo');
console.log(utcTime.toISO()); // 2024-03-10T19:30:00.000Z
console.log(nyTime.toISO()); // 2024-03-10T14:30:00.000-05:00
console.log(tokyoTime.toISO()); // 2024-03-11T04:30:00.000+09:00
// โ
Safe: DST-aware arithmetic
const beforeDST = DateTime.fromISO('2024-03-10T01:30:00', { zone: 'America/New_York' });
const afterDST = beforeDST.plus({ hours: 2 });
console.log(afterDST.toFormat('HH:mm')); // 03:30 (skipped 02:30)
Intl.DateTimeFormat for Display
const date = new Date('2024-03-10T14:30:00.000Z');
// โ
Locale-aware formatting
const usFormat = new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: 'numeric',
minute: '2-digit',
timeZoneName: 'short',
timeZone: 'America/New_York'
});
console.log(usFormat.format(date)); // March 10, 2024 at 9:30 AM EST
// โ
Multiple timezone display
const timezones = ['UTC', 'America/New_York', 'Asia/Tokyo'];
timezones.forEach(tz => {
const formatter = new Intl.DateTimeFormat('en-US', {
timeZone: tz,
timeZoneName: 'short',
hour: '2-digit',
minute: '2-digit'
});
console.log(`${tz}: ${formatter.format(date)}`);
});
// UTC: 02:30 PM UTC
// America/New_York: 09:30 AM EST
// Asia/Tokyo: 11:30 PM JST
// โ
Relative time formatting
const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });
const now = new Date();
const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
const diffDays = Math.round((yesterday - now) / (24 * 60 * 60 * 1000));
console.log(rtf.format(diffDays, 'day')); // yesterday
๐ฎ
Temporal API (Future Standard)
Note: Temporal is a Stage 3 proposal that will replace the Date API. Available via polyfill or in some modern browsers behind flags.
Temporal API Examples
// Future JavaScript with Temporal API
import { Temporal } from '@js-temporal/polyfill';
// โ
Clear, immutable, timezone-aware
const now = Temporal.Now.zonedDateTimeISO();
const utcNow = Temporal.Now.instant();
const nyNow = Temporal.Now.zonedDateTimeISO('America/New_York');
// โ
Type-safe date components
const date = Temporal.PlainDate.from('2024-03-10');
const time = Temporal.PlainTime.from('14:30:00');
const dateTime = date.toPlainDateTime(time);
// โ
Explicit timezone handling
const zonedDateTime = dateTime.toZonedDateTime('America/New_York');
const utcEquivalent = zonedDateTime.withTimeZone('UTC');
// โ
Safe arithmetic with duration
const start = Temporal.ZonedDateTime.from('2024-03-10T01:30:00[America/New_York]');
const duration = Temporal.Duration.from({ hours: 2 });
const end = start.add(duration); // Handles DST correctly
// โ
Comparison and formatting
const earlier = Temporal.Instant.from('2024-03-10T06:30:00Z');
const later = Temporal.Instant.from('2024-03-10T08:30:00Z');
console.log(Temporal.Instant.compare(earlier, later)); // -1
console.log(zonedDateTime.toString()); // 2024-03-10T14:30:00-05:00[America/New_York]
๐
Safe Parsing and Validation
Input Validation Patterns
import { DateTime } from 'luxon';
// โ
Safe ISO 8601 parsing with validation
function parseISODate(isoString) {
const dt = DateTime.fromISO(isoString);
if (!dt.isValid) {
throw new Error(`Invalid date: ${dt.invalidReason}`);
}
return dt;
}
// โ
Safe user input parsing
function parseUserDate(input, timezone = 'local') {
// Try different formats
const formats = [
'yyyy-MM-dd',
'MM/dd/yyyy',
'dd/MM/yyyy',
'yyyy-MM-dd HH:mm:ss'
];
for (const format of formats) {
const dt = DateTime.fromFormat(input, format, { zone: timezone });
if (dt.isValid) {
return dt;
}
}
throw new Error(`Unable to parse date: ${input}`);
}
// โ
API response parsing with fallback
function parseAPITimestamp(timestamp) {
// Try ISO format first
let dt = DateTime.fromISO(timestamp);
if (dt.isValid) return dt;
// Try Unix timestamp (seconds)
if (typeof timestamp === 'number') {
dt = DateTime.fromSeconds(timestamp);
if (dt.isValid) return dt;
}
// Try Unix timestamp (milliseconds)
if (typeof timestamp === 'number') {
dt = DateTime.fromMillis(timestamp);
if (dt.isValid) return dt;
}
throw new Error(`Invalid timestamp format: ${timestamp}`);
}
// Examples
try {
const date1 = parseISODate('2024-03-10T14:30:00Z');
const date2 = parseUserDate('03/10/2024', 'America/New_York');
const date3 = parseAPITimestamp(1710086400); // Unix timestamp
console.log(date1.toISO());
console.log(date2.toISO());
console.log(date3.toISO());
} catch (error) {
console.error('Parsing failed:', error.message);
}
Format Detection and Conversion
// โ
Smart format detection
function detectAndParse(input) {
// ISO 8601 patterns
const isoPattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/;
if (isoPattern.test(input)) {
return DateTime.fromISO(input);
}
// Unix timestamp (10 digits = seconds, 13 digits = milliseconds)
if (/^\d{10}$/.test(input)) {
return DateTime.fromSeconds(parseInt(input));
}
if (/^\d{13}$/.test(input)) {
return DateTime.fromMillis(parseInt(input));
}
// US format MM/DD/YYYY
if (/^\d{1,2}\/\d{1,2}\/\d{4}$/.test(input)) {
return DateTime.fromFormat(input, 'M/d/yyyy');
}
// European format DD/MM/YYYY
if (/^\d{1,2}\/\d{1,2}\/\d{4}$/.test(input)) {
return DateTime.fromFormat(input, 'd/M/yyyy');
}
throw new Error(`Unknown date format: ${input}`);
}
// โ
Safe conversion between formats
function convertDateFormat(input, fromFormat, toFormat, timezone = 'UTC') {
const dt = DateTime.fromFormat(input, fromFormat, { zone: timezone });
if (!dt.isValid) {
throw new Error(`Invalid date for format ${fromFormat}: ${input}`);
}
return dt.toFormat(toFormat);
}
// Examples
console.log(convertDateFormat('03/10/2024', 'MM/dd/yyyy', 'yyyy-MM-dd')); // 2024-03-10
console.log(convertDateFormat('2024-03-10', 'yyyy-MM-dd', 'dd/MM/yyyy')); // 10/03/2024
๐งช
Testing Time Logic
Mocking Time in Tests
import { DateTime, Settings } from 'luxon';
// โ
Mock current time with Luxon
describe('Time-dependent functionality', () => {
beforeEach(() => {
// Set fixed time for tests
Settings.now = () => new Date('2024-03-10T14:30:00Z').getTime();
});
afterEach(() => {
// Reset to real time
Settings.now = () => Date.now();
});
test('should handle current time correctly', () => {
const now = DateTime.local();
expect(now.year).toBe(2024);
expect(now.month).toBe(3);
expect(now.day).toBe(10);
});
});
// โ
Mock Date constructor with Jest
jest.useFakeTimers();
jest.setSystemTime(new Date('2024-03-10T14:30:00Z'));
test('native Date mocking', () => {
const now = new Date();
expect(now.getFullYear()).toBe(2024);
expect(now.getUTCMonth()).toBe(2); // March (0-indexed)
expect(now.getUTCDate()).toBe(10);
});
// โ
Test timezone-specific behavior
test('DST transition handling', () => {
const beforeDST = DateTime.fromISO('2024-03-10T01:30:00', {
zone: 'America/New_York'
});
const afterDST = beforeDST.plus({ hours: 2 });
// Should skip 2:30 AM during spring forward
expect(afterDST.hour).toBe(3);
expect(afterDST.minute).toBe(30);
});
Evil Test Cases
import { DateTime } from 'luxon';
describe('Evil date scenarios', () => {
test('DST spring forward gap', () => {
// 2:30 AM doesn't exist on spring forward day
const nonexistent = DateTime.fromObject({
year: 2024, month: 3, day: 10,
hour: 2, minute: 30
}, { zone: 'America/New_York' });
expect(nonexistent.isValid).toBe(false);
expect(nonexistent.invalidReason).toContain('time zone');
});
test('DST fall back ambiguity', () => {
// 1:30 AM happens twice during fall back
const firstOccurrence = DateTime.fromISO(
'2024-11-03T01:30:00-04:00' // EDT (before fall back)
);
const secondOccurrence = DateTime.fromISO(
'2024-11-03T01:30:00-05:00' // EST (after fall back)
);
expect(firstOccurrence.toMillis()).not.toBe(secondOccurrence.toMillis());
});
test('leap year edge cases', () => {
// February 29th in leap year
const feb29_2024 = DateTime.fromObject({ year: 2024, month: 2, day: 29 });
expect(feb29_2024.isValid).toBe(true);
// February 29th in non-leap year
const feb29_2023 = DateTime.fromObject({ year: 2023, month: 2, day: 29 });
expect(feb29_2023.isValid).toBe(false);
});
test('month overflow handling', () => {
// Adding months can be tricky
const jan31 = DateTime.fromObject({ year: 2024, month: 1, day: 31 });
const feb31 = jan31.plus({ months: 1 }); // February 31st doesn't exist
// Luxon handles this gracefully
expect(feb31.month).toBe(2);
expect(feb31.day).toBe(29); // 2024 is a leap year
});
test('timezone parsing edge cases', () => {
// Ambiguous timezone abbreviations
const ambiguous = [
'2024-03-10T14:30:00 CST', // Central Standard Time or China Standard Time?
'2024-03-10T14:30:00 IST', // India Standard Time or Israel Standard Time?
];
ambiguous.forEach(dateString => {
const parsed = DateTime.fromFormat(dateString, 'yyyy-MM-dd\'T\'HH:mm:ss z');
// Should handle gracefully or fail predictably
if (!parsed.isValid) {
expect(parsed.invalidReason).toBeDefined();
}
});
});
});
๐
Browser vs Node.js Considerations
Browser Environment
// โ
Browser-specific considerations
// User's timezone from browser
const userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
console.log(userTimezone); // "America/New_York"
// โ
Detect user's locale
const userLocale = navigator.language || navigator.languages[0];
console.log(userLocale); // "en-US"
// โ
Handle timezone changes (rare but possible)
window.addEventListener('focus', () => {
const currentTz = Intl.DateTimeFormat().resolvedOptions().timeZone;
if (currentTz !== lastKnownTimezone) {
// User changed timezone while app was in background
refreshTimeDisplays();
lastKnownTimezone = currentTz;
}
});
// โ
Performance: Cache formatters
const formatters = new Map();
function getFormatter(locale, options) {
const key = JSON.stringify({ locale, options });
if (!formatters.has(key)) {
formatters.set(key, new Intl.DateTimeFormat(locale, options));
}
return formatters.get(key);
}
Node.js Environment
// โ
Node.js-specific considerations
// Set timezone for entire process
process.env.TZ = 'UTC'; // Recommended for servers
// โ
Handle timezone data updates
const { DateTime } = require('luxon');
// Check if timezone data is current
function checkTimezoneData() {
try {
const dt = DateTime.local().setZone('America/New_York');
if (!dt.isValid) {
console.warn('Timezone data may be outdated');
}
} catch (error) {
console.error('Timezone data error:', error);
}
}
// โ
Server-side date handling
function createAPIResponse(data) {
return {
...data,
timestamp: DateTime.utc().toISO(), // Always UTC
timezone: 'UTC',
serverTime: DateTime.local().toISO()
};
}
// โ
Log with timezone info
function logWithTimezone(message) {
const timestamp = DateTime.utc().toISO();
console.log(`[${timestamp}] ${message}`);
}
โจ
Best Practices
API & Storage
- โข Always use ISO 8601 format for APIs
- โข Store timestamps in UTC
- โข Include timezone info in responses
- โข Use
Date.toISOString()
for serialization - โข Validate date inputs on both client and server
User Interface
- โข Display times in user's timezone
- โข Use
Intl.DateTimeFormat
for localization - โข Show timezone names/abbreviations
- โข Provide relative time when appropriate
- โข Handle timezone changes gracefully
Development
- โข Use Luxon or Temporal instead of native Date
- โข Mock time in tests for consistency
- โข Test with multiple timezones
- โข Handle DST transitions explicitly
- โข Document timezone assumptions
Performance
- โข Cache
Intl.DateTimeFormat
instances - โข Use UTC for calculations, local for display
- โข Avoid frequent timezone conversions
- โข Consider using timestamps for sorting
- โข Bundle only needed timezone data
๐
Quick Reference
// Essential Luxon patterns
import { DateTime } from 'luxon';
// Current time (always specify timezone)
const utcNow = DateTime.utc();
const localNow = DateTime.local();
const nyNow = DateTime.now().setZone('America/New_York');
// Parsing with validation
const dt = DateTime.fromISO('2024-03-10T14:30:00Z');
if (!dt.isValid) throw new Error(dt.invalidReason);
// Timezone conversion
const nyTime = utcNow.setZone('America/New_York');
const tokyoTime = utcNow.setZone('Asia/Tokyo');
// Safe arithmetic (immutable)
const start = DateTime.fromISO('2024-03-10T14:30:00Z');
const end = start.plus({ hours: 2, minutes: 30 });
// Formatting
const iso = dt.toISO(); // 2024-03-10T14:30:00.000Z
const custom = dt.toFormat('yyyy-MM-dd'); // 2024-03-10
const locale = dt.toLocaleString(); // 3/10/2024, 2:30:00 PM
// Native Date (when needed)
const date = new Date('2024-03-10T14:30:00Z');
const isoString = date.toISOString(); // Always UTC
const formatter = new Intl.DateTimeFormat('en-US', {
timeZone: 'America/New_York',
dateStyle: 'medium',
timeStyle: 'short'
});
const formatted = formatter.format(date); // Mar 10, 2024, 9:30 AM
๐
Related Resources
Documentation
Tools & Testing
- โข DST Simulator
- โข Timezone Converter
- โข Format Lab
- โข Time Crime Stories