Unix timestamps are one of those things that seem weird until suddenly they're the most natural thing in the world. Then you discover timezones and they become weird again. JavaScript's Date object has been the subject of developer complaints since Brendan Eich reportedly copied it from Java's equally broken java.util.Date — in 10 days, in 1995. Months are zero-indexed. Timezone handling is inconsistent between server and browser. Parsing ambiguous date strings is unreliable. None of this is changing for existing codebases, so understanding the fundamentals is what keeps date bugs from biting you at 2am when a user in Auckland reports a scheduling problem.
Why everything eventually becomes a number
A Unix timestamp is the number of seconds elapsed since January 1, 1970, 00:00:00 UTC — the Unix epoch. It's a single integer. No timezone ambiguity. No locale formatting. No calendar arithmetic. Just a number you can store in a database column, transmit in a JSON field, and compare with greater-than and less-than. That's why every system eventually converges on it for storing time.
Here's the gotcha: JavaScript's Date.now() returns milliseconds, not seconds. Most other systems — Unix shell, Python's time.time(), database TIMESTAMP columns, pretty much everything else — use seconds. This single mismatch is the most common source of timestamp bugs in JavaScript. Pass a millisecond value where seconds are expected and you get dates in year 53,000. Go the other way and you're stuck in January 1970. The fix is one line of code, but you have to know to do it.
// Current timestamp
Date.now(); // 1779955200000 (milliseconds)
Math.floor(Date.now() / 1000); // 1779955200 (seconds — Unix standard)
// Convert timestamp to Date
new Date(1779955200000); // Correct — milliseconds
new Date(1779955200 * 1000); // Also correct — seconds converted to ms
// Common bug: passing seconds directly
new Date(1779955200);
// → "Wed Jan 21 1970 ..." — Wrong! Interpreted as milliseconds.
// Convert Date to timestamp
const date = new Date('2026-05-25T12:00:00Z');
date.getTime(); // 1779955200000 (milliseconds)
date.valueOf(); // Same as getTime()The Date object: a single number wearing a trench coat
Under the hood, a Date object is just a millisecond timestamp with a bunch of methods bolted on. Those methods extract components in either local time or UTC — and the most confusing design decision of the whole API is that months are zero-indexed: January is 0, December is 11. Every JavaScript developer forgets this at least once. Usually at a critical moment.
// Creating dates
new Date(); // Current date/time
new Date('2026-05-25'); // ISO 8601 string (UTC)
new Date('2026-05-25T14:30:00Z'); // ISO 8601 with time (UTC)
new Date(2026, 4, 25); // Year, month (0-indexed!), day
// ^ May is 4, not 5!
// Extracting components (local time)
const d = new Date('2026-05-25T14:30:00Z');
d.getFullYear(); // 2026
d.getMonth(); // 4 (May — zero-indexed)
d.getDate(); // 25
d.getDay(); // 1 (Monday — 0=Sunday, 6=Saturday)
d.getHours(); // Depends on your time zone!
// UTC equivalents
d.getUTCHours(); // 14 (always UTC regardless of local time zone)
// ISO string — the only reliable serialization format
d.toISOString(); // "2026-05-25T14:30:00.000Z"The timezone trap that gets everyone eventually
The Date object stores time internally as UTC but displays it in the user's local timezone. The same Date object shows different hours depending on where the code runs — your laptop in Berlin, the server in Virginia, the user's browser in Sydney. When you create a date from an ISO 8601 string without a timezone suffix, the behavior isn't even consistent: Chrome and Firefox have historically differed on whether '2026-05-25' is UTC midnight or local midnight. Always include the time component.
- Store and transmit dates in UTC — ISO 8601 format with the Z suffix (e.g. '2026-05-25T14:30:00Z')
- Only convert to local time at the display layer, never at the storage or business logic layer
- Use getUTC*() methods when computing or comparing dates on the server
- new Date('2026-05-25T00:00:00Z') is unambiguous; new Date('2026-05-25') is not — don't use the latter
Hot take: most timezone bugs aren't timezone bugs at all. They're 'I stored local time in the database instead of UTC' bugs. Store UTC everywhere, convert to local time only when rendering to a human, and 90% of timezone problems disappear.
Date Formatting with Intl.DateTimeFormat
JavaScript has no built-in strftime() like Python does. The modern answer is the Intl.DateTimeFormat API, which formats dates according to locale-specific conventions without needing a library. It's been in all major browsers since around 2015 and handles the gnarly parts — localized month names, 12/24-hour clock conventions, timezone abbreviations — automatically.
const date = new Date('2026-05-25T14:30:00Z');
// Locale-aware formatting
new Intl.DateTimeFormat('en-US').format(date); // "5/25/2026"
new Intl.DateTimeFormat('en-GB').format(date); // "25/05/2026"
new Intl.DateTimeFormat('de-DE').format(date); // "25.5.2026"
// Customized formatting
new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
timeZoneName: 'short'
}).format(date);
// → "May 25, 2026, 02:30 PM EDT"
// Relative time formatting
const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });
rtf.format(-1, 'day'); // "yesterday"
rtf.format(3, 'hour'); // "in 3 hours"
rtf.format(-2, 'week'); // "2 weeks ago"Date arithmetic: where things get genuinely tricky
Adding or subtracting time from dates is more error-prone than it looks. Add 1 month to January 31 and you get February 31, which JavaScript silently rolls over to March 3 (March 2 in leap years). Add 24 hours and you might not get the same time the next day — during daylight saving transitions, days aren't 24 hours long. These edge cases don't matter until they really matter.
// Adding days — safe with timestamp math
function addDays(date, days) {
return new Date(date.getTime() + days * 86400000);
}
// Adding months — must handle month-end overflow
function addMonths(date, months) {
const result = new Date(date);
const day = result.getDate();
result.setMonth(result.getMonth() + months);
// If the day changed, we overflowed (e.g., Jan 31 + 1 month = Mar 3)
if (result.getDate() !== day) {
result.setDate(0); // Set to last day of previous month
}
return result;
}
// Difference in days
function daysBetween(a, b) {
const ms = Math.abs(b.getTime() - a.getTime());
return Math.floor(ms / 86400000);
}Temporal API: the Date replacement we've been waiting for
The Temporal API (TC39 Stage 3 proposal) completely replaces the Date object with a modern, immutable, timezone-aware design. It introduces distinct types: Temporal.PlainDate for dates without time, Temporal.PlainTime for times without dates, Temporal.ZonedDateTime for moments tied to a specific timezone, and Temporal.Duration for intervals. No more zero-indexed months. No more silent DST bugs. It's available via polyfill today.
Until Temporal ships natively everywhere, the two best library options are date-fns (tree-shakeable utility functions, excellent TypeScript support) and Luxon (immutable DateTime objects with solid timezone support). Moment.js is still on millions of sites but it's in maintenance mode — don't start new projects with it.
The things that trip everyone up eventually
- JavaScript timestamps are milliseconds; Unix timestamps are seconds — always divide/multiply by 1000 when crossing system boundaries
- Months are 0-indexed: January = 0, December = 11. This never stops being surprising.
- Use ISO 8601 format (YYYY-MM-DDTHH:mm:ssZ) for storing and transmitting dates — it's unambiguous and sorts correctly as a string
- Use date-fns or Luxon for month arithmetic or DST-sensitive calculations — the native Date object silently gets these wrong in edge cases