JAVASCRIPT / DATES, TIME, AND INTERNATIONALIZATION
Getting and setting date components
Read and modify the year, month, day, weekday and time fields of a Date, choosing the local or UTC method family and using field normalization on purpose.
What you will learn
- Read fields with the local getters or the parallel getUTC* family, never a mix
- getMonth() is 0-11, getDate() is 1-31, getDay() is a read-only 0-6 weekday
- Set several fields atomically with setFullYear(y, m, d) or setHours(h, m, s, ms)
- Use normalization for math: setDate(getDate() + n), setDate(0) for a month end
Understanding Getting and setting date components
Every Date is a single number of milliseconds since the epoch, and the component methods are just a decoder for it. getFullYear, getMonth, getDate, getDay, getHours and friends project that instant onto the host machine's local calendar, while the parallel getUTCFullYear, getUTCMonth, getUTCDate family projects the same instant onto UTC. Two numbering conventions sit side by side in that API: months are 0-based, so 0 is January and 11 is December, days of the month are 1-based, and getDay returns a weekday from 0 (Sunday) to 6 rather than a day number.
The setters run the decoder backwards, and they mutate the object in place instead of returning a new one. Each call decodes the stored instant into calendar fields for the family you picked, replaces the fields you supplied, re-encodes the result into a timestamp, and returns that timestamp as a plain number. That return value is why d.setHours(0).setMinutes(0) fails, and it is also why nearly every setter takes extra arguments, such as setHours(h, m, s, ms) and setFullYear(y, m, d), so that several fields land in a single decode-encode round.
Because the re-encoding step is arithmetic, out-of-range fields are normalized rather than rejected. setDate(0) means the day before the first, which is the last day of the previous month; setMonth(12) means January of the next year; setMinutes(90) means an hour and a half past the hour. You can lean on this deliberately, since d.setDate(d.getDate() + 10) crosses month and year boundaries with no bounds checking, but you also have to fear it: on 31 January, setMonth(1) asks for 31 February 2024 and lands on 2 March.
const d = new Date(2024, 1, 29, 13, 45, 30); // 29 Feb 2024, local time
console.log(d.getFullYear(), d.getMonth(), d.getDate(), d.getDay());
console.log(d.getHours(), d.getMinutes(), d.getSeconds());
const stamp = d.setDate(d.getDate() + 1); // mutates, returns a timestamp
console.log(typeof stamp, d.getMonth(), d.getDate());
d.setMonth(11, 31); // month and day in one call
console.log(d.getFullYear(), d.getMonth() + 1, d.getDate());
d.setDate(32); // out of range on purpose
console.log(d.getFullYear(), d.getMonth() + 1, d.getDate());Component getters and setters only decode and re-encode the single instant a Date stores, and out-of-range fields are normalized by arithmetic instead of rejected.
Worked examples
Changing the month at a month end
Shows why a lone setMonth call on the 31st overshoots, and two ways to avoid it.
const a = new Date(2024, 0, 31);
a.setMonth(1);
console.log(a.getMonth(), a.getDate());
const b = new Date(2024, 0, 31);
b.setMonth(1, 29);
console.log(b.getMonth(), b.getDate());
const c = new Date(2024, 0, 31);
c.setDate(1);
c.setMonth(1);
console.log(c.getMonth(), c.getDate());Example explained
Line 1a.setMonth(1) keeps the existing day 31, so the fields become 31 February 2024 and normalize to 2 March, month index 2.
Line 2b.setMonth(1, 29) supplies month and day together, so no impossible intermediate date is ever encoded.
Line 3c moves the day to 1 before touching the month, the usual trick when you only care about landing inside the target month.
Month lengths from day zero
Uses normalization of day 0 to find the last day of any month, leap years included.
function daysInMonth(year, month) {
return new Date(year, month + 1, 0).getDate();
}
console.log(daysInMonth(2024, 1), daysInMonth(2023, 1), daysInMonth(2024, 3));
const d = new Date(2024, 4, 15, 10, 0);
d.setDate(0);
console.log(d.getMonth(), d.getDate(), d.getHours());Example explained
Line 1new Date(year, month + 1, 0) asks for day 0 of the following month, which normalizes to the last day of the month you asked about.
Line 2Leap years need no special case: February 2024 reports 29 and February 2023 reports 28.
Line 3d.setDate(0) walks back from 15 May to 30 April, and the time fields are left alone, so getHours() is still 10.
The UTC method family
Reads and writes the same instant through UTC components so the result is identical in every time zone.
const d = new Date(Date.UTC(2024, 6, 4, 12, 0, 0));
console.log(d.getUTCDate(), d.getUTCHours(), d.getUTCDay());
d.setUTCDate(d.getUTCDate() + 30);
console.log(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate());
d.setUTCHours(0, 0, 0, 0);
console.log(d.toISOString());Example explained
Line 1Date.UTC builds the timestamp from UTC fields, so the getUTC* methods read back exactly what was passed in, wherever the code runs.
Line 2getUTCDay() returns 4 because 4 July 2024 is a Thursday; the weekday is derived from the instant, never stored.
Line 3setUTCDate(34) normalizes 34 July into 3 August, so the UTC family overflows exactly like the local one.
Line 4setUTCHours(0, 0, 0, 0) clears hours, minutes, seconds and milliseconds in a single call.
Setters return numbers, and NaN sticks
Demonstrates the return value of a setter and how one bad argument invalidates the Date for good.
const d = new Date(2024, 2, 15);
console.log(d.setHours(9) === d.getTime(), typeof d.setHours(9));
d.setMinutes(Number('half past'));
console.log(d.getMinutes(), d.getTime(), String(d));Example explained
Line 1setHours returns the new time value, the same number getTime() reports, which is why setter calls cannot be chained.
Line 2Number('half past') is NaN, and handing NaN to any setter writes NaN into the Date's internal time value.
Line 3From then on the object is permanently invalid: every getter answers NaN and no exception is ever thrown.
Important notes
There is no setDay(): the weekday is derived, so to reach a weekday you move the day of the month, for example d.setDate(d.getDate() - d.getDay()) for the previous Sunday.
setFullYear(99) really means the year 99; the two-digit shortcut only exists in the legacy new Date(99, 0, 1) form and the deprecated setYear.
Common mistakes
Printing getMonth() directly, so January appears as 0 and December as 11 and every displayed date is one month early.
Chaining setters as in d.setHours(0).setMinutes(0), which throws 'setMinutes is not a function' because setHours returned a millisecond number.
Stepping through months with setMonth() alone from a 29th, 30th or 31st, so the loop skips February entirely and reports 1-3 March instead.
Try it yourself
Change, predict, then run
In a browser console, start from const d = new Date(2024, 0, 31) and write addMonths(date, n) that returns a new Date on the same day of month when that day exists and on the last day of the target month otherwise. Verify that addMonths(d, 1) gives 29 February 2024 and addMonths(d, 3) gives 30 April 2024.
Open the JavaScript workspaceCheck your understanding
A Date holds 31 May 2024. You call d.setMonth(1) intending to move it to February. What does d hold afterwards, and why?
- 2 March 2024, because day 31 of a 29-day February is re-encoded two days past the end
- 29 February 2024, because the day is clamped to the last valid day of the target month
- 28 February 2024, because setMonth applies a fixed month length and ignores leap years
- It throws a RangeError, because 31 February is not a valid calendar date
Show answer
setMonth replaces only the month field and then re-encodes the whole field set arithmetically, so 31 February 2024 becomes 2 March. Clamping to 29 February is what library helpers such as addMonths do deliberately, but the native setters never clamp and never throw for out-of-range fields.