Get the Current Year, Month, and Day in JavaScript
To read today's year, month, and day, take Temporal.Now.plainDateISO(zone) and read .year, .month (1-based), and .day. Part of UTC vs Local Time.
Why this scenario is tricky
Reading today's year, month, and day looks trivial, and the trap is hidden in two small details of the legacy API that together produce wrong values surprisingly often. First, getMonth() is 0-based — January is 0 — so code that reads it and displays it, or builds a date from it, is off by one unless every use remembers to add one, and inevitably some use forgets. Second, the legacy getters read the host machine's zone, so "today's day" on a server in UTC can differ from the user's actual today by one, which is the off-by-one-day bug in its purest form. Both mistakes pass casual testing and surface only for users in the "wrong" part of the day or the globe.
Temporal removes both traps. Temporal.Now.plainDateISO(zone) returns a civil date whose month is 1-based (January is 1, as humans count), so there is no correction to remember and forget. And because you pass the zone explicitly, "today" is the day in the zone you name — the user's day, if you pass the user's zone — rather than a silent host default. The trickiness is entirely a legacy-API artifact; naming the zone and using 1-based months makes it disappear.
Two legacy traps meet here: getMonth() is 0-based (December is 11), and the bare getters use the host zone, so a server reports the wrong day for part of the world. Temporal's fields are 1-based and you name the zone, so both traps disappear.
Minimal working solution
Temporal.Now.plainDateISO(zone) gives today's date in the named zone, and destructuring { year, month, day } off it yields the three fields directly, with month in the intuitive 1-to-12 range. No offset correction, no zero-based month, no risk of a time component sneaking in. Passing the zone is the one deliberate choice you make, and it should almost always be the user's zone for a user-facing "today" and an explicit 'UTC' when you genuinely want the UTC calendar day for storage.
The same accessor works when you have an Instant rather than "now": project it into the zone with toZonedDateTimeISO(zone) and read the fields, or reduce to toPlainDate() first. The consistent pattern — get a civil date in an explicit zone, then read its fields — is what keeps the year, month, and day correct regardless of where the code runs, because the zone that defines the day is always visible in the call.
import { Temporal } from '@js-temporal/polyfill';
const d = Temporal.Now.plainDateISO('America/New_York');
const { year, month, day } = d; // month is 1..12
Full production version
A production helper parameterizes the zone (defaulting deliberately, often to UTC for backend jobs and to the user's zone for anything user-facing) and returns the fields, so every caller states which zone's "today" it means. This matters because the same instant is legitimately two different calendar days for two users, and a "what's today's date" endpoint that ignores the caller's zone will show the wrong day to half the world near midnight. Making the zone a required or clearly-defaulted parameter turns a silent assumption into an explicit contract.
Beyond "today," the same civil-fields pattern underlies date-part displays, form pre-fills, "on this day" features, and anniversary checks, all of which need the parts read in the user's zone to match what the user perceives. Because PlainDate exposes not just year, month, and day but also dayOfWeek, dayOfYear, weekOfYear, and daysInMonth, the same object answers a whole family of "what part of the date is this" questions consistently, all anchored to the one zone you chose. Reading them from a single civil date keeps every derived value in agreement, rather than risking one field being computed in a different zone than another.
import { Temporal } from '@js-temporal/polyfill';
function todayFields(zone = 'UTC') {
const d = Temporal.Now.plainDateISO(zone);
return { year: d.year, month: d.month, day: d.day, weekday: d.dayOfWeek };
}
Verification snippet
Common pitfalls
The first pitfall is the 0-based getMonth(): reading it without adding one, which shows or stores the wrong month, or adding one inconsistently across a codebase so some values are right and others off by one. Temporal's month is 1-based, eliminating the correction. The second is reading the fields in the host zone via the legacy getters, so "today" reflects the server's day rather than the user's — the off-by-one-day bug near midnight. Read from Temporal.Now.plainDateISO(zone) with the zone the user actually is in.
A third mistake is deriving the fields from a toISOString() slice, which is always UTC and therefore wrong for users whose local date differs at that moment. A fourth is reading year, month, and day in one zone but a related field like dayOfWeek in another, producing an internally inconsistent set; read them all from one PlainDate in one zone. Finally, remember that a bare new Date() carries a time and a zone, so any "current date" logic built on it is one refactor away from a time component leaking into a supposedly date-only value — starting from PlainDate avoids that class of regression entirely.
Frequently Asked Questions
How do I get the current month number in JavaScript?
Read Temporal.Now.plainDateISO(zone).month, which is 1-based so March is 3. The legacy new Date().getMonth() is 0-based (March is 2), which is the classic source of off-by-one month bugs.
Why does the current day differ between my server and browser?
The bare Date getters use the host zone, and servers usually run in UTC, so late-evening or early-morning users see a different calendar day. Read the fields from Temporal.Now.plainDateISO(userZone) to get their day.
How do I get the current year, month, and day in JavaScript?
Use Temporal.Now.plainDateISO(zone) and destructure { year, month, day }. The month is 1-based (January is 1), so there is no correction to remember, and passing the zone makes 'today' the day in that zone — pass the user's zone for user-facing values. This avoids both the 0-based getMonth() trap and the host-zone off-by-one that legacy getters produce.
Why is getMonth() a common source of bugs?
Because it is 0-based: January returns 0, December returns 11. Code that displays or stores the value without adding one is off by a month, and adding one inconsistently across a codebase leaves some values right and others wrong. Temporal's PlainDate.month is 1-based, matching how humans count months, so the correction — and the bugs it causes — disappear.
Why does the current day sometimes come out wrong on the server?
Because the legacy getters (getFullYear/getMonth/getDate) and toISOString() read the host machine's zone or UTC, not the user's. Near midnight the user's calendar day can differ from the server's by one, so 'today' shows the wrong date for users far from the server's zone. Reading from Temporal.Now.plainDateISO(userZone) anchors 'today' to the user's zone and fixes the off-by-one.