JAVASCRIPT / DATES, TIME, AND INTERNATIONALIZATION
Date objects and the Unix timestamp
Read and write the millisecond number inside a Date, convert it to and from Unix seconds, and compare or subtract instants without touching calendar fields.
What you will learn
- Read a Date's instant with getTime(), valueOf() or unary +; all return the same number
- Convert Unix seconds to a Date with * 1000, and back with Math.floor(ms / 1000)
- Get elapsed time as end - start; test equality with a.getTime() === b.getTime()
- Detect an Invalid Date with Number.isNaN(date.getTime()) before you use it
Understanding Date objects and the Unix timestamp
A Date instance holds exactly one piece of state: a signed integer count of milliseconds from the Unix epoch, 1970-01-01T00:00:00Z. Because that count is anchored to UTC, it names an instant on the timeline and carries no time zone or calendar with it; the year, month and hour you read out are computed from the number on demand. Negative values are earlier than 1970, so new Date(-1000) is one second before the epoch. The "Unix timestamp" that databases, JWTs and Unix tools exchange is the same count expressed in seconds, which is why the factor 1000 keeps appearing in glue code.
Since the state is a number, the language hands it to you three ways — getTime(), valueOf() and unary + — and the operators do the rest. end - start works because - asks each operand for a number, valueOf answers with the timestamp, and you get elapsed milliseconds; <, >, <= and >= compare instants for the same reason. Two exceptions bite: + asks for the "default" hint, and Date is the one built-in that answers a default hint with its string form, so date + 1 concatenates instead of adding a millisecond. And === or == between two objects compares references, never the timestamps inside them.
The number is clipped on the way in: a fractional millisecond is truncated toward zero, and anything beyond ±8.64e15 ms — 100 million days on each side of the epoch, roughly ±273,790 years — becomes NaN. A Date whose internal number is NaN is the Invalid Date, and it is not an error you can trip over immediately: the object is truthy, getTime() returns NaN, arithmetic quietly spreads NaN, JSON.stringify writes null, and toISOString() throws a RangeError somewhere much later. That is why the only reliable check is Number.isNaN(date.getTime()), applied where the data enters your code.
Epoch milliseconds are a count, not a clock reading, so the same number is the same instant everywhere; two machines in different zones that log the same getTime() are talking about the same moment even though their rendered strings differ.
// A Date holds one number: milliseconds since 1970-01-01T00:00:00Z.
const ms = 1735689600000;
const d = new Date(ms);
console.log(d.getTime());
console.log(d.valueOf() === +d, typeof +d);
console.log(d.toISOString());
// A Unix timestamp counts seconds, not milliseconds.
const unixSeconds = ms / 1000;
console.log(unixSeconds);
console.log(new Date(unixSeconds).toISOString());
console.log(new Date(unixSeconds * 1000).toISOString());A Date is a thin wrapper around one number — signed milliseconds since 1970-01-01T00:00:00Z — and its arithmetic, comparison and validity rules all follow from that number.
Worked examples
Arithmetic, comparison, and object identity
Shows that operators reach the timestamp through valueOf, while equality operators do not.
const start = Date.UTC(2026, 2, 1, 12, 0, 0);
const end = Date.UTC(2026, 2, 8, 18, 0, 0);
console.log(typeof start, end - start);
console.log((end - start) / 86400000);
const a = new Date(start);
const b = new Date(start);
console.log(a === b, a.getTime() === b.getTime());
console.log(a <= b, a >= b);Example explained
Line 1Date.UTC returns epoch milliseconds as a plain number and never builds an object, so typeof start is 'number'.
Line 2end - start forces both sides to numbers, giving 626400000 ms; dividing by 86400000 reads it as 7.25 days.
Line 3a === b is false because the two objects are separate references, even though both wrap 1772366400000.
Line 4a <= b and a >= b are both true: relational operators request a number, valueOf returns 1772366400000 on each side, and equal numbers satisfy both.
The edge of the timestamp range
Demonstrates the ±8.64e15 ms limit and how an out-of-range value becomes an Invalid Date instead of an error.
const MAX = 8.64e15; // 100,000,000 days after the epoch
console.log(new Date(MAX).toISOString());
const overflow = new Date(MAX + 1);
console.log(overflow.getTime());
console.log(String(overflow));
console.log(Number.isNaN(overflow.getTime()));
console.log(JSON.stringify({ when: overflow }));Example explained
Line 18.64e15 is the largest millisecond value a Date accepts, and toISOString prints its year with the expanded six-digit form +275760.
Line 2One millisecond past the limit is clipped to NaN, so the constructor returns an Invalid Date rather than throwing.
Line 3String(overflow) is the literal text 'Invalid Date'; the object itself is truthy, so only Number.isNaN(getTime()) detects it.
Line 4Date.prototype.toJSON returns null when the timestamp is not finite, so the bad value slips silently into serialized output.
Rounding seconds before 1970
Shows why converting negative millisecond values to Unix seconds needs Math.floor rather than truncation.
const ms = Date.UTC(1969, 6, 20, 20, 17, 40, 500);
console.log(ms);
console.log(Math.floor(ms / 1000), Math.trunc(ms / 1000));
console.log(new Date(Math.floor(ms / 1000) * 1000).toISOString());
console.log(new Date(Math.trunc(ms / 1000) * 1000).toISOString());Example explained
Line 1The instant is before the epoch, so Date.UTC returns a negative count: 14,096,539.5 seconds earlier than 1970.
Line 2Math.floor(ms / 1000) gives -14096540, the whole second that actually contains the instant.
Line 3Math.trunc rounds toward zero and gives -14096539, which is one second later than the instant it came from.
Line 4Rebuilding both values shows the effect: the floored one drops 500 ms, the truncated one lands half a second in the future.
Important notes
Epoch time pretends every day is exactly 86,400,000 ms, so leap seconds do not exist in a Date: 23:59:60 is unrepresentable and intervals spanning a leap second are off by a second from atomic time.
Date.now() follows the system clock, so it can jump backwards after an NTP correction and browsers deliberately coarsen its resolution; measure elapsed time with performance.now() instead.
Common mistakes
Feeding Unix seconds straight into new Date(): new Date(1735689600) lands on 21 January 1970, and because it is still a valid-looking date, the bug survives all the way to the UI.
Comparing two Dates with === or ==: objects for the same instant are never equal, which looks contradictory next to a <= b and a >= b both being true; compare getTime() values instead.
Writing date + 1 to add a millisecond: + prefers the string hint for Dates, so the result is a concatenated string like 'Thu Jan 01 1970 00:00:00 GMT+00001'; use new Date(date.getTime() + 1).
Try it yourself
Change, predict, then run
In a browser console, set t = Date.now(), convert it to whole Unix seconds, rebuild a Date from those seconds, and log new Date(t) - rebuilt. Run it several times and explain why the answer is always between 0 and 999.
Open the JavaScript workspaceCheck your understanding
Given const a = new Date(0) and const b = new Date(0), which statement about comparing them is correct?
- a === b is true, because both objects hold the timestamp 0
- a == b is true, because == converts both operands to numbers first
- a <= b and a >= b are both true, but a === b is false
- The comparisons all throw, because Date objects cannot be compared
Show answer
Relational operators ask each operand for a number, valueOf returns 0 on both sides, so a <= b and a >= b hold; === compares object references, which differ. The == option is tempting, but when both operands are objects no primitive conversion happens at all — it also compares references and is false.