The Date object stores an instant in UTC but reads its local fields through the host timezone, and its month is zero-based. Those two facts cause most date bugs: a stored value looks different on two machines, and new Date('2026-01-01') is not parsed the way new Date('2026-01-01T00:00') is.
Before you start
You should be comfortable with numbers, strings and the Date constructor. This article uses the standard Date API; it does not cover a date library, though the same rules apply to one.
Step-by-step walkthrough
Step 1: Store instants in UTC, format in local time
A Date is a timestamp, so persist it with toISOString() (UTC) or a numeric epoch, and convert to a locale only for display with toLocaleString. Converting to a local string before storage bakes in the server’s timezone and breaks when the value is read elsewhere.
Step 2: Remember the zero-based month
getMonth() and the Date(year, month, day) constructor count months from 0, so January is 0 and December is 11. Off-by-one month bugs are the most common date mistake in code review; use a named constant or a helper to keep call sites readable.
Step 3: Watch how strings are parsed
Date-only strings like '2026-01-01' are parsed as UTC, while date-time strings without a zone such as '2026-01-01T00:00' are parsed as local time. The inconsistency means the same pipeline can shift by hours depending on the string shape. Parse explicitly with Date.parse on an ISO string that includes a zone.
Worked scenario
Run this with Node.js. The date-only string is interpreted as UTC midnight.
const d = new Date(Date.UTC(2026, 0, 1, 0, 0, 0));
console.log(d.toISOString()); // 2026-01-01T00:00:00.000Z
console.log(new Date('2026-01-01T00:00:00Z').toISOString()); // same instant
console.log(new Date(2026, 0, 1).getMonth()); // 0 (January)Walk through the example
Date.UTC builds the instant with explicit UTC fields, so toISOString is stable everywhere. The ISO string with a trailing Z also denotes UTC. The local constructor uses the runtime timezone, which is why only the month is shown — it is always 0 for January regardless of timezone. Comparing the three shows the difference between instant-based and field-based construction.
Common mistake
Doing arithmetic with setDate/setHours on a Date used elsewhere in the app, because those mutate in place and shift by the local timezone. Another mistake is comparing formatted date strings for equality instead of comparing timestamps, which fails across daylight-saving boundaries.
Verify the behavior
Parse a date-only string and a zoned date-time string and assert they represent the intended instants. Format the same timestamp in two different timezones and confirm the underlying getTime() is unchanged. Test a daylight-saving transition day and verify that adding one day with getTime() + 86_400_000 can land on a different wall-clock hour.
Interview exercise
A server in UTC and a browser in New York both show the same timestamp. Why might the displayed dates differ by a day?
Answer and reasoning
They differ because formatting uses each host’s local timezone. A timestamp near midnight UTC is the previous evening in New York, so the calendar date can differ by one day. The fix is to decide what the value means: an absolute instant should be shown in each user’s timezone, while a calendar date should be stored as a plain YYYY-MM-DD string and treated as zoneless.
Continue learning
Compare formatting behavior in JavaScript equality and coercion and read the MDN Date reference. Test yourself with the JavaScript MCQs.