Most age calculators are wrong for at least one of these reasons:
they do nowYear – dobYear and forget to check if the birthday already happened
they treat all months like the same length
they explode on Feb 29 birthdays
they hit JavaScript’s “Jan 31 + 1 month = March” surprise
If you want an age calculator you can ship, you need a small amount of boring correctness.
This post shows a simple, testable way to calculate years + months + days between two calendar dates.
What we’re actually trying to compute
Given:
dob (date of birth)
asOf (the date you want to measure age on, defaulting to today)
We want:
years: full birthdays completed
months: full months since the last birthday
days: remaining days since that month anchor
If asOf < dob, that’s invalid input.
The two rules that prevent 90% of bugs
Rule 1: Work with “date-only”, not time
Dates in JS carry time and timezone baggage. For age, you almost always want midnight local time.
So normalize:
YYYY-MM-DD 00:00:00
Rule 2: Define your Feb 29 policy
Born on Feb 29, non-leap year: do you count their birthday on Feb 28 or Mar 1?
There’s no universal answer. Pick one and be consistent.
In this code: Feb 28.
The algorithm (simple and dependable)
compute tentative years = asOf.year – dob.year
if asOf is before birthday in asOf.year, subtract 1
set anchor = last birthday date
walk forward month-by-month from anchor, clamping day-of-month
remaining days = difference between anchor and asOf
Implementation (TypeScript)
type AgeBreakdown = { years: number; months: number; days: number };
function normalizeDateOnly(d: Date) {
return new Date(d.getFullYear(), d.getMonth(), d.getDate());
}
function daysInMonth(year: number, monthIndex0: number) {
return new Date(year, monthIndex0 + 1, 0).getDate();
}
function birthdayInYear(
dob: Date,
year: number,
feb29Rule: “FEB_28” | “MAR_1” = “FEB_28”,
) {
const m = dob.getMonth();
const day = dob.getDate();
// Feb 29 handling
if (m === 1 && day === 29) {
const isLeap = new Date(year, 1, 29).getMonth() === 1;
if (isLeap) return new Date(year, 1, 29);
return feb29Rule === “FEB_28” ? new Date(year, 1, 28) : new Date(year, 2, 1);
}
return new Date(year, m, day);
}
export function calculateAge(dobInput: Date, asOfInput: Date): AgeBreakdown {
const dob = normalizeDateOnly(dobInput);
const asOf = normalizeDateOnly(asOfInput);
if (asOf < dob) throw new Error(“asOf must be >= dob”);
// Years
let years = asOf.getFullYear() – dob.getFullYear();
const bdayThisYear = birthdayInYear(dob, asOf.getFullYear(), “FEB_28”);
if (asOf < bdayThisYear) years -= 1;
// Anchor at last birthday
const lastBirthdayYear = dob.getFullYear() + years;
let anchor = birthdayInYear(dob, lastBirthdayYear, “FEB_28”);
// Months: step forward with month-end clamping
let months = 0;
while (true) {
const nextMonthFirst = new Date(anchor.getFullYear(), anchor.getMonth() + 1, 1);
const y = nextMonthFirst.getFullYear();
const m = nextMonthFirst.getMonth();
const d = Math.min(anchor.getDate(), daysInMonth(y, m));
const candidate = new Date(y, m, d);
if (candidate <= asOf) {
months += 1;
anchor = candidate;
} else break;
}
// Days
const msPerDay = 24 * 60 * 60 * 1000;
const days = Math.floor((asOf.getTime() – anchor.getTime()) / msPerDay);
return { years, months, days };
}
Tests that catch the real failures
Don’t just test “normal” birthdays. Test the annoying dates.
import { describe, it, expect } from “vitest”;
import { calculateAge } from “./calculateAge”;
describe(“calculateAge”, () => {
it(“handles birthday not yet happened this year”, () => {
const dob = new Date(“2000-10-20”);
const asOf = new Date(“2026-02-09”);
const r = calculateAge(dob, asOf);
expect(r.years).toBe(25);
});
it(“handles month-end clamping (Jan 31)”, () => {
const dob = new Date(“2000-01-31”);
const asOf = new Date(“2000-03-01”);
const r = calculateAge(dob, asOf);
// If your month add is buggy, this often breaks.
expect(r.years).toBe(0);
expect(r.months).toBeGreaterThanOrEqual(1);
});
it(“handles Feb 29 birthdays with FEB_28 rule”, () => {
const dob = new Date(“2004-02-29”);
const asOf = new Date(“2025-02-28”);
const r = calculateAge(dob, asOf);
// Under FEB_28 policy, birthday is considered reached on Feb 28.
expect(r.years).toBe(21);
});
it(“rejects asOf before dob”, () => {
const dob = new Date(“2020-01-01”);
const asOf = new Date(“2019-12-31”);
expect(() => calculateAge(dob, asOf)).toThrow();
});
});
You can add more:
dob = 1999-12-31, asOf = 2000-01-01
dob = 2000-02-28, asOf = 2001-02-28
dob = 2000-03-31, asOf = 2000-04-30
The takeaway
If you want correct age output:
normalize to date-only
define Feb 29 behavior
clamp month ends
ship tests for weird dates
That’s it. No libraries required.
Demo (optional): https://www.calculatorhubpro.com/everyday-life/age-calculator
