Date and Age Calculations in JavaScript: Edge Cases and Best Practices
Accurate age calculation in JavaScript — handling leap years, timezones, month-end boundaries, and localized date display.
The Naive Approach and Why It Fails
Dividing the millisecond difference by the number of milliseconds in a year gives an approximate age but fails on boundary cases:
// Naive — inaccurate near birthdays
function roughAge(birthDate) {
const ms = Date.now() - new Date(birthDate).getTime();
return Math.floor(ms / (365.25 * 24 * 60 * 60 * 1000));
}This can be off by one day around the birthday because 365.25 is an approximation and does not account for which specific years in the range were leap years.
Correct Age Calculation
function calculateAge(birthDate, today = new Date()) {
let years = today.getFullYear() - birthDate.getFullYear();
let months = today.getMonth() - birthDate.getMonth();
let days = today.getDate() - birthDate.getDate();
if (days < 0) {
months--;
// Days in the previous month
const prevMonth = new Date(today.getFullYear(), today.getMonth(), 0);
days += prevMonth.getDate();
}
if (months < 0) {
years--;
months += 12;
}
return { years, months, days };
}The Leap Year Birthday Problem
People born on February 29 only have a real birthday every 4 years. By convention, their birthday is celebrated on either February 28 or March 1 in non-leap years, depending on local custom and legal jurisdiction. When calculating age for a Feb 29 birthday in a non-leap year:
function isLeapYear(year) {
return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
}
// Feb 29 birthday in a non-leap year → treat as Feb 28
function normalizeBirthday(birth, year) {
if (birth.getMonth() === 1 && birth.getDate() === 29 && !isLeapYear(year)) {
return new Date(year, 1, 28); // Feb 28
}
return new Date(year, birth.getMonth(), birth.getDate());
}Timezones and Midnight Bugs
When you parse a date string like "1990-05-15" with new Date("1990-05-15"), JavaScript interprets it as UTC midnight. If the user's local timezone is UTC-5, this resolves to May 14 at 7 PM local time — one day off.
// Parsing as local time, not UTC
function parseLocalDate(dateString) {
const [year, month, day] = dateString.split('-').map(Number);
return new Date(year, month - 1, day); // month is 0-indexed
}Always parse date-only strings (without time components) using the local constructor form to avoid timezone-induced off-by-one errors.
Days Until Next Birthday
function daysUntilBirthday(birthDate, today = new Date()) {
const nextBirthday = new Date(
today.getFullYear(),
birthDate.getMonth(),
birthDate.getDate()
);
if (nextBirthday <= today) {
nextBirthday.setFullYear(today.getFullYear() + 1);
}
const msUntil = nextBirthday - today;
return Math.ceil(msUntil / (1000 * 60 * 60 * 24));
}Localized Date Formatting with Intl
The Intl.DateTimeFormat API formats dates according to locale rules — day/month/year order, month names, and numbering systems:
const date = new Date(1990, 4, 15); // May 15, 1990
new Intl.DateTimeFormat('en-US').format(date); // 5/15/1990
new Intl.DateTimeFormat('en-GB').format(date); // 15/05/1990
new Intl.DateTimeFormat('de-DE').format(date); // 15.5.1990
new Intl.DateTimeFormat('ja-JP').format(date); // 1990/5/15
// With options
new Intl.DateTimeFormat('en-US', {
year: 'numeric', month: 'long', day: 'numeric'
}).format(date); // May 15, 1990Age in Different Cultures
In East Asian age reckoning (traditional Korean and Chinese), a person is considered 1 year old at birth and gains a year on New Year's Day rather than on their birthday. This system is officially deprecated in South Korea (as of 2023) but still used colloquially. If your application serves international users, consider whether "age" means the same thing to all of them.
Try it yourself
Calculate exact age in years, months, and days from any birthdate — with a countdown to the next birthday.
Open Age Calculator →