Quick: how old are you in days? Not approximately. Exactly. Including leap years. Go ahead and try to calculate it.
If you reached for a calculator and started multiplying your age by 365, you already got it wrong. Leap years add a day every four years -- except for years divisible by 100, unless they're also divisible by 400. The year 2000 was a leap year. 1900 was not. 2100 won't be either.
Date arithmetic looks trivial until you actually try to implement it. Then it becomes one of the most surprisingly complex problems in everyday programming.
The naive approach and why it breaks
The most common way developers calculate age is something like this:
function getAge(birthDate) {
const today = new Date();
const age = today.getFullYear() - birthDate.getFullYear();
return age;
}
This returns the wrong answer for anyone who hasn't had their birthday yet this year. Born on November 15, 1990? On March 20, 2026, this function returns 36. You're actually 35.
The "fix" usually looks like this:
function getAge(birthDate) {
const today = new Date();
let age = today.getFullYear() - birthDate.getFullYear();
const monthDiff = today.getMonth() - birthDate.getMonth();
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) {
age--;
}
return age;
}
Better, but still has edge cases. What about someone born on February 29? Are they 0 years old on March 1 of a non-leap year? When do they celebrate their birthday? Different countries actually have different legal answers to this question. In the UK and Hong Kong, a leap day baby is legally considered to have their birthday on March 1 in non-leap years. In New Zealand, it's February 28.
Months don't have consistent lengths
Calculating age in months is worse. Consider someone born on January 31. One month later is... February 28? February 29 in a leap year? March 3 (31 days after January 31)?
There's no universally correct answer. Most date libraries take the "clamp to month end" approach: one month after January 31 is February 28 (or 29). But this creates a strange asymmetry: one month after January 31 is February 28, but one month after February 28 is March 28, not March 31.
// JavaScript's Date handles this by overflowing
new Date(2024, 1, 31) // Feb 31 -> becomes March 2, 2024
JavaScript doesn't even throw an error. It silently "corrects" invalid dates by rolling over. February 31 becomes March 2 or 3 depending on the leap year. This is a source of bugs that can go undetected for months.
Time zones make everything worse
A person born at 11:30 PM on December 31 in New York was born on January 1 in London. Their age in years could differ by one depending on which time zone you use for the calculation. This matters for legal systems, medical records, and any application where age determines eligibility.
If your application stores birth dates as full timestamps with timezone information, you're probably overcomplicating things. Birth dates should almost always be stored as date-only values (YYYY-MM-DD) without a time component. The time someone was born is rarely relevant to age calculations, and attaching timezone information to it creates problems.
// Don't do this
const birthDate = new Date('1990-06-15T00:00:00Z');
// UTC midnight June 15 is still June 14 in US time zones
// Do this
const birthYear = 1990;
const birthMonth = 6; // 1-indexed
const birthDay = 15;
The libraries that get it right
Most mature date libraries handle these edge cases. Luxon, date-fns, and Day.js all have well-tested date difference functions. The Temporal API, which is making its way into JavaScript natively, is designed from the ground up to handle calendar math correctly.
// With Temporal (stage 3 proposal, available in some environments)
const birth = Temporal.PlainDate.from('1990-06-15');
const today = Temporal.Now.plainDateISO();
const age = birth.until(today, { largestUnit: 'year' });
// age.years, age.months, age.days - all correct
Temporal distinguishes between "plain" dates (no timezone) and "zoned" dates (with timezone), which solves the timezone problem at the type level. It also handles month-length variations correctly by design.
Four things developers get wrong with date arithmetic
1. Using timestamps for date-only values. If you only care about the date, don't use a datetime type. The time component introduces timezone conversion bugs that are hard to find and harder to fix.
2. Rolling their own date math. Unless you're doing it as a learning exercise, don't write your own age calculator. The edge cases (leap years, month lengths, timezone boundaries, leap seconds) are well-documented and well-solved in libraries. Use them.
3. Assuming 365.25 days per year. This is close but not exact, and the error compounds. The Gregorian calendar averages 365.2425 days per year. Over a 30-year age calculation, the difference between 365.25 and 365.2425 is small, but it matters if you're counting days precisely.
4. Not testing February 29 birthdays. If your application calculates age, write a test case for a February 29 birthday checked on February 28 and March 1 of both leap and non-leap years. If you don't have those tests, you have a bug.
Running exact calculations
For quick, precise age calculations in years, months, days, hours, and minutes -- including proper leap year handling -- I built an age calculator at zovo.one/free-tools/age-calculator that handles all the edge cases described above.
Dates are one of those domains where intuition consistently fails and the correct answer requires more care than you'd expect. Every developer thinks date math is easy until they've shipped a bug because of it. The humble age calculation is a perfect example: a problem that sounds like subtraction but is actually a small lesson in calendar systems, timezone awareness, and the gap between human expectations and mathematical precision.
I'm Michael Lip. I build free developer tools at zovo.one. 350+ tools, all private, all free.
Top comments (0)