Why we even need date math
“Born 16 May 1996. What is today’s age?” sounds trivial. It is, until you write the obvious code:
const years = now.getFullYear() - birth.getFullYear(); // wrong on day-before-birthday
const days = (now - birth) / (1000 * 60 * 60 * 24); // works! but only for days
const months = years * 12 + (now.getMonth() - birth.getMonth()); // off by 1 in mid-month
Each of these is wrong in a different way, and none of the failures are obvious until a user reports it. Tools that get date math right (ours included) handle three specific traps.
The three traps
Trap 1: Leap years
The Gregorian calendar inserts a leap day every 4 years, except on century years not divisible by 400. The rule:
isLeapYear(year) = (year % 4 === 0 && year % 100 !== 0) || (year % 400 === 0)
So 2000 was a leap year (divisible by 400) but 1900 was not (divisible by 100, not 400). Modern code rarely hits this edge — JS Date handles it — but it explains why “years × 365” undercounts by 7–8 days over a 30-year lifespan.
- True tropical year (days)
- Gregorian average (days)
- Drift per year vs reality
Trap 2: End-of-month rollover
Born Jan 31. Today is Feb 28 (non-leap). How many months old are you?
Naive code says: now.getMonth() - birth.getMonth() = 1. But you haven’t lived through a full month — you’d hit your “monthly birthday” again on Mar 31, not Feb 31 (which doesn’t exist). The right answer is 0 months, 28 days.
The fix: check the day-of-month first. If now.getDate() < birth.getDate(), you haven’t “completed” the month yet — subtract 1 from the months field and borrow the days from the previous month’s length.
let months = now.getMonth() - birth.getMonth();
let days = now.getDate() - birth.getDate();
if (days < 0) {
months -= 1; // borrow from months
const prev = new Date(now.getFullYear(), now.getMonth(), 0);
days += prev.getDate(); // days in the previous month
}
This is the exact pattern our Age in Days tool uses, and it’s directly testable: born Jan 31 2020, today Feb 28 2020 → 0 years, 0 months, 4 weeks, 0 days. Try it.
Trap 3: The day-before-birthday bug
Born May 16 1996. Today is May 15 2026 — the day before your 30th birthday. Naive code:
const years = 2026 - 1996; // 30 — but you're not 30 yet!
The fix follows directly from Trap 2: if you haven’t passed your birthday this year (months negative OR same month but days negative), subtract 1 from years. Most date libraries roll this in automatically; if you’re writing it from scratch, don’t skip it.
Many jurisdictions define “age” as completed years on the relevant date. Get this wrong in a KYC, voting-eligibility, or alcohol-age check and you’ll either let in 17-year-olds or block 18-year-olds on their birthday. Always use a tested library or our Age Calculator — never currentYear - birthYear.
Why elapsed-days math is so much simpler
If you only care about how many days (not “how many years and months”), the math is one line:
const days = Math.floor((now.getTime() - birth.getTime()) / 86_400_000);
That’s it. Days have a fixed length (modulo daylight-saving boundaries, which JavaScript’s Date hides from you). Leap years happen inside the millisecond delta, automatically.
This is why the Age in Days tool has two views side-by-side:
- Elapsed units (days, hours, minutes, seconds) — fixed-length, trivial math.
- Calendar breakdown (years, months, weeks, days) — variable-length, the math you just read about.
Both are computed from the same birth and now, but through different functions in our open-source age-in-days.ts. Take a look — it’s 80 lines, fully tested, and runs anywhere JavaScript runs.
Edge cases that surprise people
”I was born on Feb 29 — what’s my age on a non-leap year?”
Convention: your year increments on Mar 1. Most legal systems, most date libraries, and our tool all agree. The day-of-month comparison in Trap 2’s code handles it correctly — on Feb 28 (the day before your “March birthday”), you’re not a year older yet; on Mar 1, you are.
”Daylight-saving moved my birthday by an hour”
Don’t worry about it. JavaScript’s Date uses millisecond timestamps under the hood, which are insensitive to DST. The displayed time-of-day might shift by an hour twice a year, but the day you were born on doesn’t change. The tool stores the birth date as a date-only YYYY-MM-DD string, parses it to local midnight, and computes elapsed time in milliseconds — so DST never enters the math.
”What about time zones?”
The tool treats your birth date and “now” in the same timezone — the one your browser is running in. If you crossed timezones (say, born in India, now living in Sydney), the day count is your local-Sydney day count from your local-India birth midnight. For most uses that’s right; for forensic-precision date math, you’d need to specify which timezone counts as your “home” one. That’s a richer date library’s job.
Use the tool, see the math
The Age in Days tool puts all of this on one page. The live ticker shows you the elapsed view; the four tiles below show the calendar view. Both run client-side from a single tested function. Try the day-before-birthday case — it’s the bug most age calculators on the web still get wrong.