Ch. 1 · JavaScript

JavaScript Date and Timezone Pitfalls

Parse and format dates without timezone surprises: zero-based months, date-only versus date-time parsing, and why UTC storage avoids drift.

~3 min readintermediateupdated Oct 5, 2026

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)
JavaScript

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.

More in JavaScript

read ✓JavaScript · mid

JavaScript Number Precision and BigInt

Why 0.1 + 0.2 is not 0.3, where safe integer range ends, and when BigInt is the right tool for large identifiers and money.

~2 min readread →
read ✓JavaScript · easy

JavaScript Object Destructuring and Defaults

Destructure objects with renamed bindings and defaults, and learn why only undefined triggers a default while null keeps its value.

~2 min readread →
esc