A Date holds one instant as a millisecond time value. It does not retain a calendar, locale, or time-zone name.
String parsing, zero-based months, mutable setters, invalid dates, and daylight-saving transitions make plausible code fail at boundaries.
Define the input format and time zone, validate the resulting time value, keep UTC and local methods separate, and copy before using setters.
What it is and why it exists
A JavaScript Date represents one instant : a point on the timeline. Its time value is a Number of milliseconds relative to the Unix epoch , 1970-01-01T00:00:00.000Z. A valid object therefore answers “when,” but it does not itself store a place, locale, or calendar-facing label.
Applications need that neutral value to record events, compare deadlines, serialize timestamps, and turn one moment into different human-readable views. You meet Date at browser and Node boundaries, in API payloads, database records, file metadata, logs, and user interfaces. The same instant can be Friday evening in New York and Saturday morning in Shanghai.
Date is not a complete model for every time-related value. A birthday without a time, a recurring 09:00 meeting in a named time zone , and a three-hour duration are different concepts. Forcing them all into instants creates accidental UTC shifts and daylight-saving bugs.
Keep the domain distinction visible in names and schemas:
| Requirement | Data that must survive |
|---|---|
| Event already happened | Instant |
| Date on a form | Calendar year, month, and day |
| Alarm at the user’s 09:00 | Wall-clock fields and named zone |
| Task expires after 30 minutes | Duration and a suitable clock |
| Audit display | Instant plus presentation locale and zone |
Convert to Date only when an instant is actually available. A calendar-only value should not acquire midnight and a zone merely to fit this API.
The useful mental model is “time value plus projection.” UTC getters project the value onto UTC calendar fields. Local getters use the host environment’s current local-zone rules, while Intl.DateTimeFormat can project it into a named zone for display.
How it works
One numeric time value
Every valid Date has a time value in the inclusive range -8.64e15 through 8.64e15 milliseconds. The value is an integer after the specification’s clipping operation. Values outside that range, failed parses, and non-finite numeric inputs produce an invalid Date whose time value is NaN.
getTime() and valueOf() return that numeric value. Date.now() returns the current wall-clock value without allocating a Date. Subtracting two valid dates works because numeric conversion uses their time values, but strict equality still compares object identity.
| Operation | Result | Zone involved |
|---|---|---|
new Date(milliseconds) | A Date for that time value | None |
date.getTime() | Milliseconds since the epoch | None |
date.toISOString() | A canonical UTC string | UTC |
date.getFullYear() | A calendar year | Host local zone |
date.getUTCFullYear() | A calendar year | UTC |
formatter.format(date) | A localized label | Formatter configuration |
Construction chooses an interpretation
new Date() reads the current system clock. A single number is treated as milliseconds since the epoch. A single string goes through the language’s date-string parsing rules, and separate numeric components are interpreted in the host’s local time zone.
The component constructor uses zero-based months: January is 0 and December is 11. Components may overflow or underflow, so day 0 means the last day of the previous month and month 12 advances to January of the next year. Years from 0 through 99 receive a legacy 1900 offset in the component constructor and in Date.UTC().
For strings in the specified date-time format, an explicit Z means UTC and an explicit offset such as +08:00 identifies the corresponding instant. A date-only form such as 2026-04-05 is interpreted as UTC. A date-time form without an offset, such as 2026-04-05T09:00:00, is interpreted as local time; that asymmetry is a frequent review finding.
Other human-oriented strings can be implementation-defined. Treat accepting 04/05/2026, month names, or partially specified dates as an input-contract bug even if one engine happens to parse the sample.
Input forms are part of the contract
Construction syntax should match the kind of value crossing the boundary. A timestamp-bearing API should not share a parser with a birthday field, because the first identifies an instant and the second may intentionally have no time zone.
| Input | Meaning to document | Typical use |
|---|---|---|
1772951400000 | Epoch milliseconds | Internal timestamp exchange |
2026-03-08T06:30:00.000Z | Canonical UTC instant | API and log timestamp |
2026-03-08T14:30:00+08:00 | Instant with numeric offset | Offset-aware external input |
2026-03-08 | UTC midnight under Date parsing | Only when that is intended |
new Date(2026, 2, 8, 14, 30) | Host-local fields | Local UI behavior |
Do not infer a unit from a bare number. Unix timestamps are commonly exchanged in seconds, while JavaScript Date expects milliseconds. Validate the range and name values createdAtMs or createdAtSeconds so a factor-of-1000 error is visible in review.
UTC, local, and named-zone views
The local getter family includes getFullYear(), getMonth(), getDate(), and getHours(). The matching UTC methods add UTC to the name. The first family can return different fields on machines in different zones; the second returns the same fields for the same valid time value.
getTimezoneOffset() reports UTC - local in minutes for the represented instant and the host’s local zone. Its sign is therefore easy to reverse, and its value can vary across the year where daylight saving time applies. It does not reveal a zone name.
For display in a named zone, create an Intl.DateTimeFormat with an explicit locale, timeZone, and the fields you require. Formatting changes the representation, not the Date. If an application must later reproduce the user’s scheduling intent, store the zone identifier separately from the instant.
Field names that look alike
getDate() returns the day of the month from 1 through 31. getDay() returns the weekday from 0 through 6, with Sunday as 0. Generated calendar code often substitutes one for the other because their names look interchangeable.
Months from getMonth() and getUTCMonth() are zero-based, but days of the month are one-based. Hours, minutes, seconds, and milliseconds are zero-based numeric ranges. The API’s inconsistent-looking conventions are stable legacy behavior, so isolate component access behind clearly named helpers where possible.
Setter return values are numbers, not Date objects. Chaining date.setDate(...).setHours(...) therefore fails after the first call. Perform each mutation as a statement, or return the copied Date explicitly from a helper.
Serialization, arithmetic, and mutation
toISOString() returns a UTC representation and throws RangeError for an invalid date. toJSON() normally delegates to it for a valid date, but returns null for a non-finite time value. Consequently, JSON.stringify() can silently turn an invalid date property into null.
Subtracting time values measures elapsed milliseconds. Adding 86_400_000 also adds an elapsed 24 hours; it does not mean “same local time tomorrow.” Around a daylight-saving transition, those requirements can point to different instants.
All set... methods mutate the receiver and return its new numeric time value. Local setters apply local calendar rules, while UTC setters apply UTC rules. Copy first with new Date(original) when callers should keep the original value.
Storage and presentation boundaries
For an instant, an epoch-millisecond number or canonical UTC string is a stable storage representation when the surrounding schema defines it. Pick one representation per interface instead of alternating between strings, numbers, and Date objects. JSON has no date scalar, so parsed JSON gives you strings or numbers until application code validates and converts them.
Human-facing output belongs at the presentation boundary. Pass an explicit locale and named zone when consistent output matters; otherwise the host defaults become part of the result. Cache a formatter when one configuration formats many values, but do not claim a performance gain without measuring the real workload.
A zone identifier and a numeric offset are not substitutes. An offset describes one relationship to UTC, while a zone identifies a rule set whose offset can change by date. Store both the instant and the named zone when the application must reconstruct a scheduled local time.
Examples
One instant in two named zones
This example starts with an offset-explicit string, then prints the underlying value and two projections. Building the label from formatToParts() avoids depending on locale punctuation.
function wallClock(date, timeZone) {
const parts = new Intl.DateTimeFormat('en-CA', {
timeZone,
year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit', hourCycle: 'h23',
}).formatToParts(date);
const value = Object.fromEntries(parts.map(({ type, value }) => [type, value]));
return `${value.year}-${value.month}-${value.day} ${value.hour}:${value.minute}`;
}
const release = new Date('2026-03-08T06:30:00.000Z');
console.log(release.getTime());
console.log(release.toISOString());
console.log(wallClock(release, 'America/New_York'));
console.log(wallClock(release, 'Asia/Shanghai'));1772951400000
2026-03-08T06:30:00.000Z
2026-03-08 01:30
2026-03-08 14:30All four lines describe the same instant. The named zones affect only the last two labels. Neither release nor its time value changes during formatting.
Validate a canonical API timestamp
Parsing success is not enough for a strict API contract because some out-of-range calendar fields normalize. This validator first checks the required shape, then round-trips the result through toISOString() to reject normalization.
function parseCanonicalUtc(value) {
const shape = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
if (typeof value !== 'string' || !shape.test(value)) {
throw new TypeError('expected YYYY-MM-DDTHH:mm:ss.sssZ');
}
const date = new Date(value);
if (Number.isNaN(date.getTime()) || date.toISOString() !== value) {
throw new RangeError('timestamp is not a real UTC instant');
}
return date;
}
for (const input of [
'2026-02-28T12:30:00.000Z',
'2026-02-30T12:30:00.000Z',
'02/28/2026',
]) {
try {
console.log('OK', parseCanonicalUtc(input).toISOString());
} catch (error) {
console.log(error.name, error.message);
}
}OK 2026-02-28T12:30:00.000Z
RangeError timestamp is not a real UTC instant
TypeError expected YYYY-MM-DDTHH:mm:ss.sssZThe second input has the right text shape but no real February 30. The third may be accepted by an engine’s permissive parser, but it is outside this API’s contract and is rejected before parsing.
Separate calendar recurrence from elapsed time
New York moves from UTC−05:00 to UTC−04:00 on the represented boundary. Two appointments at 01:30 on consecutive calendar dates are therefore only 23 elapsed hours apart, while adding exactly 24 hours lands at 02:30 local time.
function clockTime(date, timeZone) {
return new Intl.DateTimeFormat('en-US', {
timeZone,
hour: '2-digit', minute: '2-digit', hourCycle: 'h23',
}).format(date);
}
const zone = 'America/New_York';
const firstAppointment = new Date('2026-03-08T01:30:00-05:00');
const nextCalendarDay = new Date('2026-03-09T01:30:00-04:00');
const afterTwentyFourHours = new Date(firstAppointment.getTime() + 86_400_000);
const elapsedHours = (nextCalendarDay - firstAppointment) / 3_600_000;
console.log(`calendar recurrence: ${elapsedHours} elapsed hours`);
console.log(`next local time: ${clockTime(nextCalendarDay, zone)}`);
console.log(`after 24 hours: ${clockTime(afterTwentyFourHours, zone)}`);calendar recurrence: 23 elapsed hours
next local time: 01:30
after 24 hours: 02:30The explicit offsets make the input instants deterministic. In production scheduling data, the named zone is still needed because a fixed offset does not contain future daylight-saving rules.
Add months without mutating or overflowing
Raw month setters preserve the current day number and then normalize overflow. A clamped operation must move to a safe day first, find the target month’s end, and restore no more than that month can hold.
function addUtcMonthsClamped(date, months) {
const result = new Date(date);
const originalDay = result.getUTCDate();
// Starting on day 1 prevents the old day from overflowing the target month.
result.setUTCDate(1);
result.setUTCMonth(result.getUTCMonth() + months);
const endOfTargetMonth = new Date(result);
endOfTargetMonth.setUTCMonth(endOfTargetMonth.getUTCMonth() + 1, 0);
result.setUTCDate(Math.min(originalDay, endOfTargetMonth.getUTCDate()));
return result;
}
const issuedAt = new Date('2026-01-31T09:00:00.000Z');
const oneMonthLater = addUtcMonthsClamped(issuedAt, 1);
const twoMonthsLater = addUtcMonthsClamped(issuedAt, 2);
console.log(issuedAt.toISOString());
console.log(oneMonthLater.toISOString());
console.log(twoMonthsLater.toISOString());
console.log(issuedAt === oneMonthLater);2026-01-31T09:00:00.000Z
2026-02-28T09:00:00.000Z
2026-03-31T09:00:00.000Z
falseUTC methods make this helper independent of the host’s local daylight-saving rules. The final false confirms that the function returned a different object instead of mutating issuedAt.
Pitfalls
Accepting whatever Date.parse() accepts
Fix: choose a documented wire format, require an explicit offset for instants, and validate both the input shape and the resulting time value. Use a round-trip check when the contract requires canonical UTC text. Do not claim that a regular expression alone proves a calendar date exists.
Testing an invalid object for truthiness
Fix: test Number.isNaN(date.getTime()) at the boundary and decide whether invalid input throws or returns a typed failure. Validate before formatting, comparing, or storing the value.
Mixing UTC and local accessors
Fix: name the intended view and use one method family throughout an operation. For a named-zone display, use one configured Intl.DateTimeFormat instead of manually adding an offset.
Mutating a shared date
Fix: copy with new Date(input) and specify the overflow policy in the helper name and tests. Cover month ends, leap days, negative increments, and both sides of a daylight-saving transition when local setters are involved.
Treating 24 hours as one calendar day
Fix: decide whether the requirement is elapsed duration or calendar recurrence. Use time-value arithmetic for the former; for the latter, retain the relevant zone and apply calendar rules deliberately.
Comparing object identity instead of time values
Fix: validate both operands and compare left.getTime() === right.getTime(). If the requirement is “same local date,” compare fields in an explicitly chosen zone instead; that is a different question.
Parsing at the contract boundary
The language guarantees support for its date-time string format, the form emitted by toISOString(). It also requires a few round-trip invariants for strings produced by toString() and toUTCString() when milliseconds are zero. Support for unrelated formats is not a portable contract, even if popular engines agree on a particular input today.
The missing-offset rule deserves its own test. A date-only string is UTC, but a date-time string without Z or an offset is local. On a machine west of UTC, new Date('2026-01-01').getDate() can therefore report the previous local calendar date, while new Date('2026-01-01T00:00:00') begins at local midnight.
Normalization is separate from grammar. For example, an engine can turn a syntactically shaped February 30 into a March instant. A form field that represents a civil date should validate year, month, and day as calendar fields; an API instant can require canonical text and compare it with toISOString() as the example does.
An explicit numeric offset identifies an instant, not a durable zone rule. 2026-07-01T09:00:00-04:00 says how that occurrence relates to UTC. It does not say whether the meeting belongs to America/New_York, nor which offset a recurrence should use after rules change.
Component construction avoids string parsing but brings different rules. Months start at zero, fields normalize, and years 0 through 99 map to 1900 through 1999. To construct those early years, start from a valid date and use setUTCFullYear() or setFullYear() explicitly.
Calendar arithmetic across zone changes
Elapsed arithmetic is straightforward after both inputs are validated: subtract their time values and keep the unit visible. It is suitable for expiry windows, latency, and “exactly 30 minutes later.” A system clock adjustment can still affect two readings of Date.now(), so a monotonic clock such as performance.now() is the better source for measuring an interval within one running context.
Calendar arithmetic starts with fields and a zone. “Tomorrow at the same local time” means advance the date in that calendar and resolve the resulting wall-clock fields under that zone’s rules. A spring transition can remove local times; an autumn transition can make one local time correspond to two instants, so the product needs a gap and overlap policy.
Local setDate(getDate() + 1) applies the host local zone and generally preserves the local clock fields while advancing the date. The elapsed difference can be 23 or 25 hours across a transition. UTC setters avoid host-zone transitions, but UTC calendar arithmetic does not implement a named zone’s calendar.
Intl.DateTimeFormat can display an instant in an IANA zone such as Asia/Shanghai. It does not parse a wall-clock time in that zone or perform named-zone calendar addition. If scheduling requires those operations, model the zone as data and use an API whose contract defines gaps, overlaps, and recurrence behavior.
Range, precision, and clock semantics
The permitted time-value range is exactly ±8.64e15 milliseconds from the epoch. new Date(8.64e15) is valid, while one millisecond beyond it is invalid. This clipping limit also keeps every permitted integral millisecond exactly representable by a JavaScript Number.
The model counts milliseconds with every civil day treated as 86_400_000 milliseconds. It does not represent leap seconds as distinct instants. Systems that exchange higher-resolution timestamps must define how to round or preserve sub-millisecond data outside Date.
Date.now() reads the system wall clock, which administrators or time synchronization can adjust. Its resolution may also be reduced by an environment for privacy. Use it to stamp an event when a wall-clock timestamp is required; use a monotonic timing source for deadlines and elapsed performance measurements within a process or page.
Tests that expose environment assumptions
A reliable date test fixes every input that would otherwise come from the environment. Use literal instants, pass the locale and zone to formatters, and avoid assertions against new Date() unless the clock is injected or controlled. This keeps a developer laptop, CI worker, and production container from silently testing different calendars.
Partition parsing tests by contract rather than collecting random malformed strings. Cover the exact canonical form, a missing offset, a wrong unit, an impossible calendar date, a value just outside the permitted range, and the expected non-string behavior. Assert both the failure type and that no partially converted value escapes.
Calendar arithmetic tests should include the shortest target month, a leap-year February, a year boundary, and negative movement. If local or named-zone behavior matters, select real gap and overlap transitions from the relevant zone rules. Assert the intended local fields and the elapsed milliseconds because either result alone can hide the wrong semantic choice.
Useful invariants include:
- Copying a valid date preserves its time value but changes object identity.
- Formatting in different zones never changes the original time value.
- A successful canonical parser round-trips through
toISOString()unchanged. - A helper documented as non-mutating leaves its input’s time value unchanged on success and failure.
Environment control is part of the test setup, not an unstated assumption. For host-local methods, run a targeted test process with a known TZ setting where the runtime supports it. For named-zone display, pass timeZone directly and ensure the deployment carries the required internationalization and zone data.
4 questions · 1 predict-the-output · 1 spot-the-bug