Date and Time: Common Pitfalls and How to Fix Them

TL;DR

Always store dates in UTC. Display in user's timezone. Never use new Date() for parsing. Avoid date math with native Date. Use date-fns or Luxon. ISO 8601 for serialization. Timezones are hard - libraries handle them.

Our scheduled emails sent at the wrong time. Users in PST got them at 6am instead of 9am. The code said "9am" but didn't specify timezone. Database stored local time, not UTC. Chaos.

I spent a week fixing every date/time bug in our codebase. Converted everything to UTC. Added proper timezone handling. No more "wrong time" bugs since.

Here's every date/time mistake I've made and how to fix them.

The new Date() Problem

// This looks simple but is dangerous
const date = new Date('2026-02-09');
console.log(date);
// Chrome: Sun Feb 09 2026 00:00:00 GMT-0800 (PST)
// Server: Sun Feb 09 2026 08:00:00 GMT+0000 (UTC)
// Different times on different machines!

Problem: new Date() parsing is inconsistent across browsers and timezones.

// What you think you're getting
new Date('2026-02-09'); // Feb 9 at midnight local time

// What you're actually getting depends on format:
new Date('2026-02-09');           // UTC midnight (becomes 4pm PST on Feb 8)
new Date('2026/02/09');           // Local midnight
new Date('February 9, 2026');     // Local midnight
new Date('2026-02-09T00:00:00');  // Local midnight
new Date('2026-02-09T00:00:00Z'); // UTC midnight

Never use new Date() for parsing. Use a library.

Always Store UTC

// BAD - Storing local time
const now = new Date();
await db.query('INSERT INTO events (time) VALUES (?)', [now]);
// Stored: "2026-02-09 14:30:00" (PST? UTC? Unknown!)

// GOOD - Store UTC
const now = new Date();
await db.query('INSERT INTO events (time) VALUES (?)', [now.toISOString()]);
// Stored: "2026-02-09T22:30:00.000Z" (UTC, unambiguous)

Rule: Always store dates in UTC.

Displaying Times

// Get from database (UTC)
const event = await db.query('SELECT time FROM events WHERE id = ?', [id]);
// event.time = "2026-02-09T22:30:00.000Z"

// Display in user's timezone
const userDate = new Date(event.time);

// BAD - Shows UTC time
console.log(userDate.toString());
// "Sun Feb 09 2026 22:30:00 GMT+0000 (UTC)"

// GOOD - Shows user's local time
console.log(userDate.toLocaleString());
// "2/9/2026, 2:30:00 PM" (PST = UTC-8)

// BETTER - Explicit timezone
console.log(userDate.toLocaleString('en-US', {
    timeZone: 'America/Los_Angeles',
    dateStyle: 'medium',
    timeStyle: 'short'
}));
// "Feb 9, 2026, 2:30 PM"

The Date Constructor Is Broken

// Month is 0-indexed (why??)
new Date(2026, 0, 15); // January 15
new Date(2026, 1, 15); // February 15
new Date(2026, 11, 15); // December 15

// This is confusing and error-prone
const month = 2; // February?
new Date(2026, month, 1); // March 1 (month 2 = March!)

// Date overflow
new Date(2026, 1, 30); // Feb 30 doesn't exist
// Result: March 2 (30 days after Feb 1)

Never use the Date constructor directly.

Timezone Hell

// User schedules email for "9am tomorrow"
const tomorrow9am = new Date();
tomorrow9am.setDate(tomorrow9am.getDate() + 1);
tomorrow9am.setHours(9, 0, 0, 0);

await db.query('INSERT INTO scheduled_emails (send_at) VALUES (?)',
    [tomorrow9am.toISOString()]
);

// Problem 1: If user is in PST, stored time is 17:00 UTC
// Problem 2: If user travels to EST, they expect 9am EST, not 9am PST
// Problem 3: Daylight saving time changes everything

Solution: Store timezone separately

await db.query(
    'INSERT INTO scheduled_emails (send_at_utc, timezone) VALUES (?, ?)',
    ['2026-02-10T17:00:00Z', 'America/Los_Angeles']
);

// When sending, convert to user's current timezone
const sendTime = moment.tz(sendAtUtc, timezone);
if (moment().isSame(sendTime, 'hour')) {
    sendEmail();
}

Daylight Saving Time

// November 3, 2024: Clocks fall back (PST)
const date1 = new Date('2024-11-03T01:30:00-07:00'); // PDT (summer)
const date2 = new Date('2024-11-03T01:30:00-08:00'); // PST (winter)
// Same time displayed, different actual times!

// Adding days can skip hours
const nov2 = new Date('2024-11-02T12:00:00');
const nov3 = new Date(nov2);
nov3.setDate(nov3.getDate() + 1);
// Expected: Nov 3 at 12:00
// Actual: Nov 3 at 11:00 (lost an hour due to DST)

DST is why you need libraries.

Using date-fns (Recommended)

const {
    format,
    parseISO,
    addDays,
    addHours,
    differenceInDays,
    isAfter,
    isBefore,
    startOfDay,
    endOfDay
} = require('date-fns');

// Parsing (safe)
const date = parseISO('2026-02-09T22:30:00Z');
console.log(date);
// Date object with correct time

// Formatting
format(date, 'yyyy-MM-dd'); // "2026-02-09"
format(date, 'MMM d, yyyy'); // "Feb 9, 2026"
format(date, 'h:mm a'); // "2:30 PM"
format(date, 'EEEE, MMMM do, yyyy'); // "Sunday, February 9th, 2026"

// Date math (safe)
const tomorrow = addDays(date, 1);
const later = addHours(date, 3);

// Comparisons
isAfter(date1, date2);
isBefore(date1, date2);
differenceInDays(date1, date2);

// Start/end of day
const start = startOfDay(date); // Today at 00:00:00
const end = endOfDay(date);     // Today at 23:59:59

Using Luxon (Timezone Aware)

const { DateTime } = require('luxon');

// Current time in UTC
const now = DateTime.utc();
console.log(now.toISO());
// "2026-02-09T22:30:00.000Z"

// Current time in specific timezone
const nowPST = DateTime.now().setZone('America/Los_Angeles');
console.log(nowPST.toISO());
// "2026-02-09T14:30:00.000-08:00"

// Parse ISO string
const date = DateTime.fromISO('2026-02-09T22:30:00Z');

// Parse with timezone
const datePST = DateTime.fromISO('2026-02-09T14:30:00', {
    zone: 'America/Los_Angeles'
});

// Format
date.toFormat('yyyy-MM-dd'); // "2026-02-09"
date.toFormat('MMM d, yyyy'); // "Feb 9, 2026"
date.toLocaleString(DateTime.DATETIME_FULL);
// "February 9, 2026 at 2:30 PM PST"

// Date math (timezone aware)
date.plus({ days: 1 });
date.plus({ hours: 3 });
date.minus({ weeks: 2 });

// Convert timezones
const utcDate = DateTime.utc(2026, 2, 9, 14, 30);
const pstDate = utcDate.setZone('America/Los_Angeles');
console.log(pstDate.toFormat('h:mm a')); // "6:30 AM"

// Relative time
date.toRelative(); // "in 5 minutes"
date.toRelativeCalendar(); // "tomorrow"

Common Patterns

Scheduling for User's Local Time

const { DateTime } = require('luxon');

// User wants email at 9am their time
async function scheduleEmail(userId, localTime, timezone) {
    // Parse in user's timezone
    const userTime = DateTime.fromFormat(localTime, 'h:mm a', {
        zone: timezone
    });

    // Convert to UTC for storage
    const utcTime = userTime.toUTC();

    await db.query(
        'INSERT INTO scheduled_emails (user_id, send_at_utc, timezone) VALUES (?, ?, ?)',
        [userId, utcTime.toISO(), timezone]
    );
}

// Usage
await scheduleEmail(123, '9:00 AM', 'America/Los_Angeles');
// Stores: "2026-02-10T17:00:00Z" in database
// Sends: At 9am PST for this user

Displaying Relative Times

const { formatDistanceToNow } = require('date-fns');

const posted = parseISO('2026-02-09T14:00:00Z');

// "5 minutes ago"
console.log(formatDistanceToNow(posted, { addSuffix: true }));

// Update periodically for real-time feel
setInterval(() => {
    document.getElementById('time').textContent =
        formatDistanceToNow(posted, { addSuffix: true });
}, 60000); // Update every minute

Date Ranges

const { DateTime } = require('luxon');

// Start and end of day
const start = DateTime.now().startOf('day');
const end = DateTime.now().endOf('day');

// This week
const weekStart = DateTime.now().startOf('week');
const weekEnd = DateTime.now().endOf('week');

// This month
const monthStart = DateTime.now().startOf('month');
const monthEnd = DateTime.now().endOf('month');

// Date range for queries
await db.query(
    'SELECT * FROM events WHERE created_at BETWEEN ? AND ?',
    [monthStart.toISO(), monthEnd.toISO()]
);

Countdown Timer

const { intervalToDuration, formatDuration } = require('date-fns');

function countdown(targetDate) {
    const now = new Date();
    const target = parseISO(targetDate);

    const duration = intervalToDuration({
        start: now,
        end: target
    });

    return formatDuration(duration, {
        format: ['days', 'hours', 'minutes', 'seconds']
    });
}

// "5 days 3 hours 45 minutes 12 seconds"
console.log(countdown('2026-02-15T00:00:00Z'));

Business Days

const { addBusinessDays, isWeekend } = require('date-fns');

// Add 5 business days (skip weekends)
const dueDate = addBusinessDays(new Date(), 5);

// Check if weekend
if (isWeekend(date)) {
    console.log('It\'s the weekend!');
}

Database Storage

-- PostgreSQL: Use TIMESTAMPTZ (timestamp with timezone)
CREATE TABLE events (
    id SERIAL PRIMARY KEY,
    created_at TIMESTAMPTZ DEFAULT NOW(),
    scheduled_for TIMESTAMPTZ
);

-- Always stores in UTC internally
-- Returns in connection's timezone setting

-- MySQL: Use DATETIME with explicit UTC
CREATE TABLE events (
    id INT PRIMARY KEY AUTO_INCREMENT,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    scheduled_for DATETIME
);

-- Store UTC times only
-- Add "_utc" suffix to column names for clarity

API Serialization

// Always use ISO 8601 format
const event = {
    id: 123,
    title: 'Meeting',
    start: new Date().toISOString(), // "2026-02-09T22:30:00.000Z"
    end: new Date().toISOString()
};

// Parse on client
const start = new Date(event.start);

// Or with date-fns
const start = parseISO(event.start);

Frontend Display

// React component
function EventTime({ timestamp, timezone }) {
    const [localTime, setLocalTime] = useState('');

    useEffect(() => {
        const dt = DateTime.fromISO(timestamp).setZone(timezone);
        setLocalTime(dt.toLocaleString(DateTime.DATETIME_FULL));
    }, [timestamp, timezone]);

    return <time dateTime={timestamp}>{localTime}</time>;
}

// Usage
<EventTime
    timestamp="2026-02-09T22:30:00Z"
    timezone="America/Los_Angeles"
/>
// Displays: "February 9, 2026 at 2:30 PM PST"

Testing with Fixed Times

// Jest: Mock current time
jest.useFakeTimers();
jest.setSystemTime(new Date('2026-02-09T12:00:00Z'));

// Your code runs with fixed time
const now = new Date();
console.log(now); // Always "2026-02-09T12:00:00Z"

// Advance time
jest.advanceTimersByTime(3600000); // +1 hour

// date-fns: No mocking needed, pass date explicitly
const result = addDays(new Date('2026-02-09'), 1);
expect(result).toEqual(new Date('2026-02-10'));

Common Mistakes

Mistake 1: Storing Local Time

// BAD - What timezone is this?
await db.query('INSERT INTO events (time) VALUES (?)',
    ['2026-02-09 14:30:00']
);

// GOOD - Store UTC with explicit timezone
await db.query('INSERT INTO events (time) VALUES (?)',
    ['2026-02-09T22:30:00Z']
);

Mistake 2: Comparing Dates as Strings

// BAD - String comparison
if (date1 > date2) // Might work, might not

// GOOD - Compare as Date objects
if (new Date(date1) > new Date(date2))

// BETTER - Use library
if (isAfter(parseISO(date1), parseISO(date2)))

Mistake 3: Ignoring Timezones

// BAD - Assumes user's timezone
const tomorrow9am = new Date();
tomorrow9am.setDate(tomorrow9am.getDate() + 1);
tomorrow9am.setHours(9, 0, 0, 0);

// GOOD - Explicit timezone
const tomorrow9am = DateTime.now()
    .setZone('America/Los_Angeles')
    .plus({ days: 1 })
    .set({ hour: 9, minute: 0, second: 0 });

Mistake 4: Date Math Without Libraries

// BAD - Native date math is broken
const date = new Date('2026-02-09');
date.setMonth(date.getMonth() + 1); // March 9
date.setMonth(date.getMonth() + 1); // April 9
// Seems fine...

const jan31 = new Date('2026-01-31');
jan31.setMonth(jan31.getMonth() + 1);
console.log(jan31); // March 3 (Feb 31 overflows!)

// GOOD - Use library
addMonths(parseISO('2026-01-31'), 1); // Feb 28

Mistake 5: Not Handling Invalid Dates

// BAD - Silent failure
const date = new Date('invalid');
console.log(date); // Invalid Date
console.log(date.toISOString()); // RangeError!

// GOOD - Check validity
const date = new Date(input);
if (isNaN(date.getTime())) {
    throw new Error('Invalid date');
}

// BETTER - Use library
const date = DateTime.fromISO(input);
if (!date.isValid) {
    throw new Error(date.invalidReason);
}

Quick Reference

// Current time
new Date()                          // Local time (use carefully)
Date.now()                          // Timestamp (milliseconds)
DateTime.utc()                      // UTC time (Luxon)

// Parsing
parseISO('2026-02-09T22:30:00Z')   // date-fns
DateTime.fromISO('2026-02-09...')   // Luxon

// Formatting
format(date, 'yyyy-MM-dd')          // date-fns
date.toFormat('yyyy-MM-dd')         // Luxon
date.toISOString()                  // ISO 8601

// Date math
addDays(date, 1)                    // date-fns
date.plus({ days: 1 })              // Luxon

// Comparisons
isAfter(date1, date2)               // date-fns
date1 > date2                       // Luxon

// Timezones
date.setZone('America/Los_Angeles') // Luxon

The Bottom Line

Date and time are hard. Native JavaScript Date is broken. Use libraries.

Always store UTC in database. Display in user's timezone.

Never use new Date() for parsing - results vary by browser and timezone.

Use date-fns or Luxon - they handle timezones, DST, and edge cases.

Test with fixed times - don't let tests break at midnight.

We sent emails at 6am instead of 9am because we didn't handle timezones. Spent a week fixing every date bug. Use UTC, use libraries, save yourself the pain.