← Blog
Utilities8 min

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, 1990

Age 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.