GAP·MAP

Dates & time

[ junior depth ]

A Date stores an instant

A JavaScript Date contains one integral number: milliseconds relative to 1970-01-01T00:00:00Z, the epoch. A positive value is after the epoch and a negative value is before it. The number identifies an instant. It does not include a saved locale or an IANA time zone such as Europe/Berlin.

const epoch = new Date(0);

epoch.getTime();     // 0
epoch.toISOString(); // "1970-01-01T00:00:00.000Z"

Wrong "A Date stores year, month, day, and the time zone used to create it."

Right A Date stores a timestamp. Methods interpret that timestamp as UTC or in the host environment's local time zone.

UTC and local methods can show different dates

The getUTC... family reads calendar fields in UTC. Methods such as getFullYear(), getMonth(), and getDate() use the host's local time zone. The timestamp stays unchanged when either family reads it.

const instant = new Date("2020-09-30T00:00:00.000Z");

instant.getUTCFullYear(); // 2020
instant.getUTCMonth();    // 8, because Date months are zero-based
instant.getUTCDate();     // 30

In a zone behind UTC, the local getters can report September 29 for that same instant. This is the source of many "off by one day" reports. The Date did not lose a day. The program asked for the calendar date in a different time zone.

A missing offset changes parsing

The standardized Date Time String Format has a surprising split. A date-only string without an offset is interpreted as UTC. A date-time string without an offset is interpreted as local time.

Date.parse("2019-01-01");          // midnight UTC
Date.parse("2019-01-01T00:00:00"); // midnight in the host's local time zone
Date.parse("2019-01-01T00:00:00Z"); // midnight UTC, stated explicitly

For an instant crossing an API boundary, include Z or a numeric offset. toISOString() emits a UTC string with Z.

const createdAt = new Date("2026-07-12T09:30:00+03:00");
const wireValue = createdAt.toISOString();
// "2026-07-12T06:30:00.000Z"

A birthday is different. 1990-06-15 often means a calendar date with no instant and no time zone. Turning it into midnight UTC creates instant semantics that the data did not have.

const birthday = "1990-06-15";
const [year, month, day] = birthday.split("-").map(Number);

// Keep these as calendar fields for validation and display.

Format for people with Intl.DateTimeFormat

Intl.DateTimeFormat performs language-sensitive formatting. Pass the locale and time zone when the output contract requires them. Defaults depend on the runtime's locale and time zone.

const formatter = new Intl.DateTimeFormat("en-GB", {
  dateStyle: "full",
  timeStyle: "short",
  timeZone: "Europe/Berlin",
});

formatter.format(new Date("2026-07-12T06:30:00Z"));

Machine interchange and user display have different jobs. Use an unambiguous timestamp for an instant on the wire. Use Intl.DateTimeFormat with an intentional locale and time zone at the display boundary.

[ middle depth ]

The parser has a narrow guarantee

ECMAScript specifies a Date Time String Format based on the ISO 8601 extended calendar date format. Date.parse() first tries that grammar. If the input does not conform, an implementation may use its own heuristics or formats.

// Standardized shapes
Date.parse("2026-07-12");
Date.parse("2026-07-12T09:30:00Z");
Date.parse("2026-07-12T09:30:00+03:00");

// Avoid depending on implementation-specific parsing.
Date.parse("07/12/2026 09:30");

An unrecognizable string returns NaN. A Date holding NaN is an invalid Date. A successful parse does not prove that the input used the standardized grammar because an implementation may have accepted an extension.

const timestamp = Date.parse(input);

if (Number.isNaN(timestamp)) {
  throw new TypeError("Unparseable date string");
}

const instant = new Date(timestamp);

Use separate syntax validation when an input contract requires the standardized format.

Wrong "Every ISO-looking string without Z uses local time."

Right In the specified grammar, an offsetless date-only form uses UTC, while an offsetless date-time form uses local time.

That split makes this conversion unsafe for calendar-only data:

// The input means a birthday, but Date turns it into an instant at UTC midnight.
const birthday = new Date("1990-06-15");

Preserve the domain instead:

const birthday = "1990-06-15";

function readIsoCalendarDate(value) {
  const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
  if (!match) throw new TypeError("Expected YYYY-MM-DD");

  return {
    year: Number(match[1]),
    month: Number(match[2]),
    day: Number(match[3]),
  };
}

That example validates the string shape only. Calendar validation must also reject impossible month and day combinations before accepting external input.

Component constructors use local time

With two or more numeric arguments, the Date constructor interprets components in local time. Months are zero-based. Components can overflow or underflow into adjacent units.

const localNoon = new Date(2026, 6, 12, 12); // July is month 6
const nextYear = new Date(2026, 12, 1);       // January 1, 2027
const previousDay = new Date(2026, 6, 0);     // June 30, 2026

Date.UTC() accepts similar components but interprets them as UTC and returns a timestamp rather than a Date.

const timestamp = Date.UTC(2026, 6, 12, 12);
const utcNoon = new Date(timestamp);

Date setters mutate the object. A helper that receives a Date can change the caller's value if it calls setDate(), setMonth(), or another setter. Copy before mutation when legacy code requires these methods.

function addOneLocalDay(input) {
  const result = new Date(input.getTime());
  result.setDate(result.getDate() + 1);
  return result;
}

A time-zone offset belongs to an instant

getTimezoneOffset() reports the difference between UTC and local time in minutes for the represented instant. The value can differ across the year because offsets reflect daylight-saving transitions and historical changes.

function offsetsForHostZone(year) {
  return {
    january: new Date(year, 0, 1).getTimezoneOffset(),
    july: new Date(year, 6, 1).getTimezoneOffset(),
  };
}

Do not cache one current offset as the rule for a named time zone. A numeric offset also does not carry the identity of an IANA zone.

For elapsed time in milliseconds, subtract timestamps. Calendar arithmetic requires more care because local days can have different lengths across offset transitions.

const elapsedMilliseconds = end.getTime() - start.getTime();

Make formatting inputs explicit

Intl.DateTimeFormat can select the locale, calendar fields, hour cycle, and time zone. Its resolvedOptions() method exposes the locale and options that initialization computed. formatToParts() returns locale-specific tokens for custom layouts.

const formatter = new Intl.DateTimeFormat("en-GB", {
  year: "numeric",
  month: "2-digit",
  day: "2-digit",
  hour: "2-digit",
  minute: "2-digit",
  hour12: false,
  timeZone: "Australia/Sydney",
  timeZoneName: "short",
});

const parts = formatter.formatToParts(
  new Date("2026-07-12T06:30:00Z"),
);
const zone = formatter.resolvedOptions().timeZone;

Do not parse a localized display string back with Date.parse(). The specification does not require Date.parse() to accept the output of toLocaleString().

[ senior depth ]

Name the value before choosing an API

"Date and time" hides several domains. An audit record needs a unique instant. A birthday needs a calendar date without a time zone. A recurring meeting at 09:00 in Europe/Berlin needs a recurrence rule, an authoritative local time, and the named zone whose offset rules apply.

Legacy Date covers the first case. Its timestamp identifies an instant, while its component methods expose only UTC or the host's local zone. It has no calendar-date-only type and does not retain an arbitrary named time zone.

Wrong "Store everything in UTC and timezone bugs disappear."

Right UTC is suitable for an instant. Adding a UTC midnight to a birthday changes the data model, and reducing a recurring zoned meeting to one instant loses its local-time rule.

Temporal separates the domains

As of July 12, 2026, TC39 marks the Temporal proposal as Stage 4. MDN still marks the Temporal global as limited availability because some widely used browsers do not support it. Check the target runtimes or use a production-ready polyfill approved for the application before shipping code that assumes a global Temporal.

Temporal gives each domain a separate type:

  • Temporal.Instant identifies a unique point on the timeline.
  • Temporal.PlainDate represents a calendar date without a time or time zone.
  • Temporal.ZonedDateTime combines an instant, a time zone, and a calendar.
  • Temporal.PlainDateTime has calendar and clock fields without a time zone.
const deployedAt = Temporal.Instant.from("2026-07-12T06:30:00Z");
const birthday = Temporal.PlainDate.from("1990-06-15");
const meeting = Temporal.ZonedDateTime.from(
  "2026-07-12T09:00:00+02:00[Europe/Berlin]",
);

That meeting value represents one dated occurrence. A recurring series also needs its recurrence rule, local time, and zone stored separately. Resolve each occurrence only after its calendar date is known:

const meetingTime = Temporal.PlainTime.from("09:00");
const meetingZone = "Europe/Berlin";

const occurrenceDate = Temporal.PlainDate.from("2026-10-25");
const occurrence = occurrenceDate.toPlainDateTime(meetingTime)
  .toZonedDateTime(meetingZone, { disambiguation: "reject" });

Temporal objects are immutable. Operations return new objects instead of changing the receiver.

const original = Temporal.PlainDate.from("2026-07-12");
const tomorrow = original.add({ days: 1 });

original.toString(); // "2026-07-12"
tomorrow.toString(); // "2026-07-13"

The distinction also affects arithmetic. Temporal.Instant can add time-based durations such as seconds. Calendar units need a calendar and, when local days matter, a time zone. Convert to a zoned value before calendar arithmetic that must follow local offset changes.

Gaps and repeated local times need a policy

An offset transition can make a local wall time nonexistent or make it correspond to two instants. Legacy Date resolves these cases with behavior equivalent to Temporal's disambiguation: "compatible": it chooses the earlier instant when two exist and moves forward by the gap when none exists.

Temporal makes the policy selectable when a local value is converted to Temporal.ZonedDateTime. The choices include earlier, later, compatible, and reject.

const fields = {
  timeZone: "America/New_York",
  year: 2024,
  month: 3,
  day: 10,
  hour: 2,
  minute: 30,
};

const meeting = Temporal.ZonedDateTime.from(fields, {
  disambiguation: "reject",
});
// Throws RangeError because this local time is in the spring-forward gap.

reject is useful when the product must ask the user what to do. compatible preserves Date-compatible behavior. The application owns this policy because the correct choice depends on what the scheduled value means.

Offset and named zone answer different questions

An offset such as +02:00 states how a local date-time maps to UTC for one occurrence. A named zone such as Europe/Berlin supplies rules used to determine offsets for particular instants. Future occurrences that must stay at 09:00 Berlin time need the local recurrence fields and zone identity. One offset copied from the first occurrence is insufficient.

For a wire-format instant, keep an explicit Z or numeric offset:

const instant = Temporal.Instant.from("2026-07-12T06:30:00Z");
const payload = instant.toJSON();

For localized display, keep using the internationalization APIs and pass the intended locale and zone. Formatting is a presentation operation. It does not change the represented instant.

const formatter = new Intl.DateTimeFormat("de-DE", {
  dateStyle: "full",
  timeStyle: "short",
  timeZone: "Europe/Berlin",
});

formatter.format(new Date("2026-07-12T06:30:00Z"));

At system boundaries, document which domain crosses the boundary: instant, calendar date, local date-time, or zoned date-time. A field called date leaves the most important part unspecified.

dig deeper

Primary sources behind these notes - the specs and official docs worth reading in full.

beta

The interviewer part is in the works.

The diagnostic, personal maps, and AI mock interviews are being finished right now. The notes stay free either way. Leave an email and you'll get the first-cohort invite, plus a month of Pro when it opens.

more JavaScript

was this useful?