# Date

Source: https://codewiki.com/javascript/date-object/

> - **what**: A `Date` holds one instant as a millisecond time value. It does not retain a calendar, locale, or time-zone name.
> - **trap**: String parsing, zero-based months, mutable setters, invalid dates, and daylight-saving transitions make plausible code fail at boundaries.
> - **fix**: 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.

<!-- quick -->

```javascript
// file: instant_views.js
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'));
```

```text
1772951400000
2026-03-08T06:30:00.000Z
2026-03-08 01:30
2026-03-08 14:30
```

<!-- /quick -->

All 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.

```javascript
// file: strict_timestamps.js
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);
  }
}
```

```text
OK 2026-02-28T12:30:00.000Z
RangeError timestamp is not a real UTC instant
TypeError expected YYYY-MM-DDTHH:mm:ss.sssZ
```

The 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.

```javascript
// file: calendar_vs_elapsed.js
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)}`);
```

```text
calendar recurrence: 23 elapsed hours
next local time: 01:30
after 24 hours: 02:30
```

The 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.

```javascript
// file: clamped_months.js
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);
```

```text
2026-01-31T09:00:00.000Z
2026-02-28T09:00:00.000Z
2026-03-31T09:00:00.000Z
false
```

UTC 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

> **Pitfall:** A generated parser often passes user text straight to `new Date(value)`. Non-standard strings may be implementation-defined, and even specified-looking fields can normalize instead of being rejected.

**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

> **Pitfall:** `new Date('invalid')` creates a truthy object. Most getters return `NaN`, `toISOString()` throws, and JSON serialization can turn it into `null` later.

**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

> **Pitfall:** Combining `getUTCFullYear()` with `getMonth()` or constructing local components before serializing to UTC can assemble fields from different calendar views.

**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

> **Pitfall:** Setters change the original object, so a helper can silently alter a value still owned by its caller. Month and day setters also normalize overflow rather than clamping it.

**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

> **Pitfall:** Dividing milliseconds by `86_400_000` measures elapsed 24-hour units, not dates on a local calendar. Adding that constant can change the displayed hour across a daylight-saving boundary.

**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

> **Pitfall:** Two separately constructed dates for the same instant are different objects, so `left === right` is `false`. Relational comparison happens to coerce them, which makes an equality-only bug easy to miss.

**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.

<!-- deep -->

## 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.

<!-- /deep -->

[Checkpoint: javascript/date-object](https://codewiki.com/javascript/date-object/#checkpoint)

## Further reading

- [MDN: `Date`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date)
- [MDN: `Date.parse()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/parse)
- [MDN: `Intl.DateTimeFormat`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat)
- [MDN: `Date.prototype.setDate()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/setDate)
