JavaScript Date instances are mutable. What do setter methods such as `setMonth()` return and what do they do to the object, and what is the result of `new Date(2025, 0, 31).setMonth(1)`?
answer
- setters change the object itself
- they hand back a number
- 31 February has to go somewhere
- copy before you mutate an argument
- equality needs getTime()
basics
~20 sDate setters mutate the object in place and return the new time value as a number, not the Date. Setting month 1 on 31 January 2025 asks for 31 February, which normalises to 3 March — so month arithmetic on a month-end date silently skips a month.
solid answer
~50 s`setMonth()`, `setDate()`, `setHours()` and the rest all mutate the receiver and return the object's new time value as a plain number, so chaining like `d.setMonth(1).getDate()` throws — you are calling a Date method on a number. Because a Date is a mutable reference, passing one to a function that calls a setter changes the caller's object too; the defensive copy is `new Date(d)` or `new Date(d.getTime())`. The second half is the rollover trap: `new Date(2025, 0, 31).setMonth(1)` sets the month to February while the day stays 31, and 31 February normalises forward to 3 March 2025. Naive "add one month" loops therefore skip February entirely from a 29th, 30th or 31st. The fix is to clamp the day yourself — set the day to 1 first, change the month, then clamp to the shorter month's length.
code
javascript · 11 linesconst jan31 = new Date(2025, 0, 31);
const returned = jan31.setMonth(1);
console.log(typeof returned); // 'number' — the new time value
console.log(jan31.toDateString()); // Mon Mar 03 2025 — 31 Feb overflowed
const a = new Date(2025, 0, 1);
const b = new Date(2025, 0, 1);
console.log(a == b); // false — distinct objects
console.log(a <= b && a >= b); // true — numeric coercion
console.log(a.getTime() === b.getTime()); // true — the right checkgo deeper
Know that Date setters change the object itself and return a number rather than the Date, so you cannot chain them, and that copying a Date means new Date(d), not assignment.
Explain that setters normalise after writing one field, so 31 January plus setMonth(1) becomes 3 March, and show the copy-then-clamp pattern along with getTime() comparison instead of ==.
Point out where shared mutable Dates cause real incidents — a Date stored on a cached object or captured in a closure being rewritten by a helper — and enforce copy-on-entry plus explicit clamp-or-overflow policy for month arithmetic.
Decide as a matter of design whether instants cross module boundaries as mutable Date objects at all, and make the clamp-versus-overflow rule for billing-style month arithmetic an explicit, tested product decision rather than an artefact of normalisation.
## Setters mutate and return a number Every `Date` mutator — `setFullYear`, `setMonth`, `setDate`, `setHours`, `setMinutes`, `setSeconds`, `setMilliseconds`, `setTime`, and the `setUTC*` family — changes the object in place and returns the object's **new time value**, a number of milliseconds since the epoch. It does not return the Date. ```js const d = new Date(2025, 0, 15); const r = d.setMonth(5); typeof r; // 'number' r === d.getTime(); // true d.getMonth(); // 5 — d itself changed d.setMonth(5).getDate(); // TypeError: d.setMonth(...).getDate is not a function ``` This single design choice produces two distinct classes of bug: broken chaining, and shared mutable state. ## Shared mutable state Date objects are references. Assigning one does not copy it, and any function you hand it to can rewrite the instant under you. ```js function endOfDay(d) { d.setHours(23, 59, 59, 999); // mutates the caller's object return d; } const start = new Date(2025, 2, 15); const end = endOfDay(start); start.getHours(); // 23 — the caller's "start" is gone ``` The same trap appears when a Date is stored on an object, put in an array, or captured in a closure: everything holding the reference observes the mutation. The two idiomatic copies are `new Date(d)` — the constructor accepts a Date and takes its time value — and `new Date(d.getTime())`. A function that computes a derived instant should copy first and return the copy, never touch its argument. Equality follows from the same reference nature: two Date objects for the same instant are never `===` or `==` to each other, because object comparison is by identity. Compare `a.getTime() === b.getTime()` (or `+a === +b`). Relational operators do work — `a < b` coerces both to numbers through `ToPrimitive` with the number hint — which makes `<` reliable while `==` is not, a genuinely surprising asymmetry. ## The month-end rollover Setters write one field and then normalise the whole date, exactly like the constructor. `setMonth(1)` writes February into the month field while leaving day-of-month at 31; 31 February does not exist, so normalisation carries the surplus forward: ```js const d = new Date(2025, 0, 31); // 31 Jan 2025 d.setMonth(1); // asks for 31 Feb d.toDateString(); // 'Mon Mar 03 2025' (2025 is not a leap year) ``` In a leap year the same call lands on 2 March. Run this in a loop to walk months from a 31st and you skip February and sometimes land on the wrong month for every 30-day month too. The same applies to `setFullYear()` on 29 February: moving to a non-leap year yields 1 March. The defensive form clamps the day: ```js function addMonths(date, n) { const d = new Date(date); const day = d.getDate(); d.setDate(1); // park on a day every month has d.setMonth(d.getMonth() + n); const lastDay = new Date(d.getFullYear(), d.getMonth() + 1, 0).getDate(); d.setDate(Math.min(day, lastDay)); return d; } addMonths(new Date(2025, 0, 31), 1).toDateString(); // 'Fri Feb 28 2025' ``` Whether clamping to the 28th or overflowing to 3 March is "right" is a product decision — a monthly subscription usually wants the clamp — but the language's default is overflow, and you have to choose deliberately. ## The multi-argument setters Several setters accept extra fields, and using them avoids intermediate invalid states: `setHours(h, m, s, ms)`, `setMinutes(m, s, ms)`, `setSeconds(s, ms)`, `setFullYear(y, monthIndex, day)`, `setMonth(monthIndex, day)`. Setting year, month and day in one `setFullYear(2025, 1, 28)` call avoids the transient 31 February that a sequence of single-field calls would create. This matters: `d.setMonth(1); d.setDate(28);` on 31 January passes through 3 March and then sets the date to 28 March — the wrong month entirely. ## Practical rules 1. Treat Date as if it were immutable: copy on entry, never mutate an argument. 2. Never chain off a setter; it returns a number. 3. Use the multi-argument setters when several fields must change together. 4. Compare instants with `getTime()`, not `==`. 5. For month and year arithmetic on month-end dates, decide clamp-versus-overflow explicitly and implement it; do not let normalisation decide for you.
- Why does `d1 < d2` work for Dates while `d1 == d2` fails for two objects holding the same instant?Relational operators coerce both operands with the number hint, so each Date becomes its time value and the comparison is numeric. Loose and strict equality between two objects compare identity instead, so distinct objects are never equal however identical their instants. Compare `d1.getTime() === d2.getTime()` or `+d1 === +d2`.
- What goes wrong with `d.setMonth(1); d.setDate(28);` on a Date holding 31 January?The first call overflows to 3 March, and the second then sets day 28 of March — you end up in the wrong month, not on 28 February. Setting the fields together with `d.setMonth(1, 28)`, or parking the day at 1 before changing the month, avoids the transient invalid date.
- How do you copy a Date, and why is assignment not enough?Use `new Date(d)` or `new Date(d.getTime())`. Assignment copies the reference, so both names point at one mutable object and any setter call through either is visible through the other. Spreading or Object.assign does not help either — a Date's state lives in an internal slot, not in enumerable own properties.
saying these in an interview costs you the question
- Thinks setMonth() returns the Date so it can be chained
- Assumes passing a Date to a function is safe from mutation
- Expects setMonth(1) on 31 January to give 28 February
- Compares two Dates with == and expects true
- Copies a Date with the spread operator or Object.assign