Get the Number of Days in a Month in JavaScript
To get the number of days in a month, read Temporal.PlainDate.from({year, month, day: 1}).daysInMonth — it returns 28 or 29 for February automatically. Part of Leap Year Algorithms.
Why this scenario is tricky
The number of days in a month is one of those facts everyone "knows" — thirty days hath September — and yet it is a frequent source of bugs, because the one month that varies, February, varies according to the leap-year rule, and because the legacy idiom for computing it relies on a piece of arithmetic trivia that reads as a mistake. You cannot hard-code a lookup table of twelve numbers, because February is 28 or 29 depending on the year, so any table needs the year anyway. And you cannot naively "add a month and see how far you went" without tripping over the month-end overflow behavior that this very quantity governs.
The classic legacy trick — new Date(year, month, 0).getDate() — exploits the fact that day 0 of the next month rolls back to the last day of the current month, and it works, but it is opaque: a reader has to know that the month argument here is effectively 1-based and that day 0 means "the day before day 1." Temporal replaces this cleverness with an honest property, daysInMonth, which reads exactly as what it is and already accounts for leap years. The trickiness, in other words, is entirely in the legacy approach; the modern one removes it.
Days-in-month is really the leap-year question in disguise: only February varies, and only by the leap-year rule. A hard-coded [31,28,31,...] table is wrong every fourth year. Temporal exposes the answer as a property so you never re-implement the rule.
Minimal working solution
Temporal.PlainDate.from({ year, month, day: 1 }).daysInMonth returns the correct count for that month in that year, 28 or 29 for February as the leap rule requires, and 30 or 31 for the others. It is a plain integer property, not a computation you assemble, so there is nothing to get subtly wrong — no modulo, no off-by-one, no month-index confusion. Because PlainDate is a civil type with no zone, the answer is identical everywhere, which matters if you were ever tempted to derive the count from a zoned Date whose month could differ near midnight in some zones.
The legacy new Date(year, month, 0).getDate() form is worth understanding because you will meet it in existing code, but note its quirk: the month argument there is treated as 1-based precisely because day 0 pushes back into the previous month. That reliance on an off-by-one interacting with an overflow is exactly the kind of implicit knowledge that makes legacy date code hard to read and easy to break. Prefer the daysInMonth property in new code and leave a comment when you must touch the legacy trick.
import { Temporal } from '@js-temporal/polyfill';
const n = Temporal.PlainDate.from({ year: 2024, month: 2, day: 1 }).daysInMonth; // 29
Full production version
In production, "days in a month" is rarely the end goal; it is a building block for month grids, "last day of month" calculations, proration, and validation of a user-entered day. Deriving the last day as first.with({ day: first.daysInMonth }) gives you a PlainDate for the month-end without any special case, and it composes cleanly with the range logic used elsewhere — a month's half-open bounds are its first day and the first day of the next month. Keeping the count as a property of a civil date, rather than a free-floating number, means these downstream uses inherit the leap-year correctness automatically.
The same property exists on the other calendar-bearing types (PlainYearMonth has daysInMonth too), which is convenient when you are working at month granularity and do not want to pin a specific day. And because Temporal supports non-Gregorian calendars, daysInMonth respects whichever calendar the date is in — a month in a lunar calendar reports its own 29 or 30, and a leap month in the Hebrew calendar reports correctly — so the same code generalizes beyond the Gregorian assumption baked into a hard-coded table. That generality is a quiet benefit of reading the count from the calendar rather than computing it yourself.
// Legacy trick: day 0 of the next month is the last day of this month.
function daysInMonth(year: number, month1to12: number): number {
return new Date(year, month1to12, 0).getDate(); // month is 1-based here by design
}
Verification snippet
Common pitfalls
The first pitfall is a hard-coded array of month lengths that omits the leap-year case for February, producing a count that is right eleven months of the year and silently wrong every fourth February. Read daysInMonth, which folds in the leap rule. The second is month-index confusion: legacy Date months are 0-based, Temporal months are 1-based, and the new Date(year, month, 0) trick relies on a 1-based reading — mixing these up shifts the result by a whole month. Pick the Temporal property and the confusion disappears.
A third pitfall is computing the count from a zoned Date and reading local components, which can report the wrong month for values near midnight in certain zones, and therefore the wrong day count. Use a zoneless PlainDate so the month is unambiguous. Finally, avoid deriving the month-end by adding a month and subtracting a day on a legacy Date, because that path invokes the overflow behavior you are trying to characterize and can misbehave at year boundaries; with({ day: daysInMonth }) is the direct, correct expression.
Frequently Asked Questions
How do I get the number of days in a month?
Read Temporal.PlainDate.from({year, month, day: 1}).daysInMonth. It returns 30 or 31 for fixed months and 28 or 29 for February according to the leap-year rule, so you never hard-code the count.
What is the new Date(year, month, 0) trick?
Day 0 of a month is the last day of the previous month, so new Date(year, month, 0).getDate() gives the number of days in that month. Note the month argument here is effectively 1-based because you are asking for the month before month+1.
How do I get the number of days in a month in JavaScript?
Use Temporal.PlainDate.from({ year, month, day: 1 }).daysInMonth, a plain integer property that returns 28 or 29 for February per the leap-year rule and 30 or 31 otherwise. It needs no modulo or lookup table and, because PlainDate is zoneless, gives the same answer everywhere. The legacy equivalent is new Date(year, month, 0).getDate(), which works but relies on day 0 rolling back to the previous month's last day.
Why is February sometimes 28 and sometimes 29 days?
Because of the Gregorian leap-year rule: a year has a February 29 if it is divisible by 4, except century years, which must also be divisible by 400. So 2024 and 2000 are leap years (29 days) while 1900 and 2100 are not (28 days). A hard-coded table of month lengths is wrong every fourth February unless it consults the year; daysInMonth folds the rule in for you.
How do I get the last day of a month with Temporal?
Compose it from daysInMonth: first.with({ day: first.daysInMonth }), where first is the PlainDate for day 1 of the month. This gives a PlainDate for the month-end with no special case and correct leap-year handling. Avoid deriving it by adding a month and subtracting a day on a legacy Date, which invokes month-end overflow behavior and can misbehave at year boundaries.