Working out somebody's age sounds like subtraction. Take today, take their birthday, subtract. And if all you want is a number of days, it genuinely is that easy. The difficulty starts the moment you want the answer in years, months and days — because months are not a fixed length, and the obvious algorithm is wrong in ways that only show up on particular dates.
Why days-divided-by-365 fails
The quickest approach is to subtract the two timestamps and divide. It gives a plausible-looking number and it is wrong almost immediately: it ignores leap years, drifting by about a day every four years. By the time someone is forty, the answer is off by roughly ten days.
Worse, it cannot produce months at all. February is 28 or 29 days, other months are 30 or 31, and there is no average that is correct for a specific pair of dates. Forty-five days is one month and a fortnight in February, but one month and two weeks in March.
The obvious algorithm, and why it breaks
The standard approach is to subtract each component and borrow when one goes negative, the way you would with columns of digits:
years = to.year - from.year
months = to.month - from.month
days = to.day - from.day
if (days < 0) { months -= 1; days += /* some month's length */ } The problem is the comment. Which month's length do you borrow? Both obvious answers are wrong, and each is wrong on a different date.
Borrow the month before the end date, and 15 January to 10 March works out correctly at one month and 24 days. But 31 January to 1 March borrows February's 29 days against a shortfall of 30, and the day count is still negative. The bug surfaces as a nonsense answer such as "1 month and −1 days".
Borrow the start date's month instead and that case is fixed — but now 15 January to 10 March borrows January's 31 days and reports one month and 26 days, when the true answer is 24. A tidier-looking bug, and harder to notice.
Settle the months first
The fix is to stop treating days as a leftover to patch up. Decide how many whole months have elapsed, walk the start date forward by exactly that many, then count the real calendar days remaining:
months = whole months between the dates
anchor = startDate + months // clamped to the month end
days = actual days from anchor to endDate Because the final step measures a genuine distance between two real dates, it is correct by construction no matter how long the intervening months were. There is no month length to choose and therefore no wrong choice to make.
What "plus one month" means
Walking forward by whole months needs its own rule, because 31 January plus one month has no obvious answer — February has no 31st. The near-universal convention is to clamp to the last day of the target month, so 31 January plus one month is 28 or 29 February.
The alternative, letting the date overflow into 2 or 3 March, would make monthly anniversaries drift forward every time they passed a short month. A subscription that renewed on the 31st would gradually walk into the middle of the month.
Clamping has a visible consequence in the other direction. Ask for 31 March to 30 April and our calculator says 30 days, not one month, because the day number never reaches the 31st. That is the same rule most date libraries apply.
Leap-day birthdays
Someone born on 29 February has a real birthday only every four years. In common years a convention is required, and jurisdictions differ: most of the English-speaking world treats 1 March as the anniversary, while some other legal systems use 28 February.
This is not a trivia question. It decides the day on which a person born on 29 February legally turns eighteen. Our calculator follows the 1 March convention.
The timezone bug hiding in every date field
One last trap, and it is the one that most often makes an age calculator wrong by
a day. A date input gives you a string like 1990-05-14. Passing that
straight to JavaScript's Date parses it as UTC midnight.
For anyone west of Greenwich, UTC midnight is still the previous evening locally — so the date renders as the 13th, and every calculation is a day out. Half the planet gets the wrong answer, and the developer, if they are in Europe, never sees it.
The fix is to build the date from its parts rather than parse the string, which pins it to local midnight. A birthday is a calendar date, not an instant in time, and it should be handled as one.
FAQ
Why can you not work out an age by dividing the number of days by 365?
Because that ignores leap years and uneven month lengths. Dividing by 365 drifts by roughly one day every four years, so someone in their forties comes out about ten days wrong. It also cannot express an answer in months, since months vary between 28 and 31 days and no single average is correct for a specific pair of dates.
How old is someone born on 29 February in a non-leap year?
There is no 29 February in a common year, so a convention has to be chosen. Most English-speaking jurisdictions treat 1 March as the legal anniversary, which is what our calculator uses. Some other systems use 28 February instead. The difference only matters on that one day of the year, but it does change which day someone legally turns eighteen.
Why is 31 January to 1 March one month and one day rather than one month and two days?
Because one month after 31 January is the end of February, not a date in March. February has no 31st, so the month step clamps to the last day available, which is 29 February in a leap year. From there, 1 March is one further day. Clamping to the end of the month is what keeps monthly anniversaries from drifting forward over time.
Why does the calculator say 31 March to 30 April is thirty days rather than one month?
Because the day of the month never reaches the 31st in April, so a full calendar month has not elapsed. The same rule is used by most date libraries. It is the counterpart of the clamping rule: clamping decides where a month lands when you add one, while this decides whether a month has completed when you measure backwards.
Related guides
- Unit conversion and why temperature is different — the same theme, where the obvious arithmetic quietly fails.
- UUID v4 versus v7 — more on timestamps, and why encoding time correctly matters.
Try it now: Free Age Calculator