Cómo calcular el tiempo entre dos fechas en JavaScript
Restar dos fechas da los días totales en una línea. Descomponerlos en años, meses y días es otra conversación: los meses tienen distinta longitud, el 31 de enero lleva a territorio pantanoso y los años bisiestos añaden un día que cambia el resultado.

Parece una operación sencilla. Tienes una fecha de inicio y una fecha de fin, y quieres saber cuánto tiempo ha pasado entre las dos. En días es trivial. En años, meses y días empieza a haber trampas. No muchas, pero las que hay son las suficientes para que el resultado salga mal si no se piensan bien.
La operación fácil: días totales
La diferencia en días es directa. Un objeto Date en JavaScript es internamente un número de milisegundos desde el 1 de enero de 1970. Restar dos de ellos da milisegundos; dividir entre los milisegundos de un día da días.
function diasTotales(inicio, fin) {
return Math.round((fin - inicio) / (1000 * 60 * 60 * 24));
}
El Math.round en lugar de Math.floor sirve para absorber el ruido de los cambios de hora por el horario de verano: en la transición de invierno a verano hay días de 23 horas, y en la inversa, de 25. Redondear da siempre el resultado calendario correcto.
Con los inputs de tipo date del HTML, el valor llega como cadena YYYY-MM-DD. Para evitar problemas de zona horaria hay que parsear en local:
const inicio = new Date(valorInput + 'T00:00:00');
Si se hace new Date('2024-01-31') sin la parte horaria, JavaScript lo interpreta como UTC y puede aparecer el día anterior dependiendo del navegador y la zona horaria del usuario.
El problema de los meses
Los meses no tienen todos los mismos días. Enero tiene 31, febrero tiene 28 o 29, y el resto oscilan entre 28 y 31. Eso hace que «un mes» no sea una unidad fija: no son siempre 30 días, no son siempre 31.
La consecuencia práctica es que «sumar un mes a una fecha» es una operación que necesita definirse con cuidado. Sumar un mes a 15 de enero da 15 de febrero, sin problema. Sumar un mes a 31 de enero da... el 31 de febrero, que no existe. La respuesta razonable es ir al último día del mes de destino: el 28 o 29 de febrero según el año.
Esa ambigüedad es exactamente la que hay que resolver para calcular correctamente años+meses+días.
El caso más traicionero: el 31 de enero
Toma este ejemplo concreto: ¿cuánto tiempo hay entre el 31 de enero de 2024 y el 1 de marzo de 2024?
La respuesta en días es 29 (2024 es bisiesto, febrero tiene 29 días). En años+meses+días, la respuesta intuitiva es 1 mes y 1 día: del 31 de enero, avanzando un mes, llegas al 29 de febrero (el 31 de febrero no existe, se captura al último día del mes); del 29 de febrero al 1 de marzo hay un día más.
El error habitual es hacer la resta de componentes ingenuamente:
// Esto está MAL para casos como el 31 de enero
let meses = fin.getMonth() - inicio.getMonth(); // 2 - 0 = 2
let dias = fin.getDate() - inicio.getDate(); // 1 - 31 = -30
// «Corrección» estándar: si dias < 0, resta un mes y suma días del mes anterior
if (dias < 0) {
meses--;
dias += new Date(fin.getFullYear(), fin.getMonth(), 0).getDate(); // +29
// Resultado: dias = -30 + 29 = -1. Sigue siendo negativo. El algoritmo falla.
}
El problema es matemático: marzo empieza el día 1, enero el día 31. La diferencia de días es -30. Febrero tiene 29 días. Al «devolver» un mes tomando prestados 29 días, el saldo sigue siendo negativo. El algoritmo clásico no converge para este caso.
La función addMonths
La solución es no intentar reconstruir la diferencia a partir de componentes, sino hacerlo al revés: avanzar desde la fecha de inicio hasta la de fin usando meses correctamente definidos, y contar los días que sobran.
Para eso se necesita una función que sume meses capturando al último día del mes si se desborda:
function addMonths(fecha, meses) {
const resultado = new Date(fecha);
const diaOriginal = resultado.getDate();
resultado.setMonth(resultado.getMonth() + meses);
// Si setMonth() desbordó al mes siguiente (31 de feb → 2 o 3 de mar),
// retrocede al último día del mes correcto.
if (resultado.getDate() !== diaOriginal) {
resultado.setDate(0);
}
return resultado;
}
Cuando JavaScript ejecuta setMonth(1) sobre un 31 de enero, el resultado es el 31 de febrero, que no existe. El motor avanza automáticamente al 2 o 3 de marzo dependiendo del año. La comparación getDate() !== diaOriginal detecta ese desbordamiento, y setDate(0) retrocede al último día del mes anterior, que es el último día de febrero.
La función completa
Con addMonths disponible, el algoritmo queda así:
function calcularDiferencia(inicio, fin) {
// Garantizar que inicio <= fin
if (inicio > fin) { const t = inicio; inicio = fin; fin = t; }
let años = fin.getFullYear() - inicio.getFullYear();
let meses = fin.getMonth() - inicio.getMonth();
// Si el día de fin es anterior al de inicio, aún no se ha completado el mes
if (fin.getDate() < inicio.getDate()) {
meses--;
}
if (meses < 0) {
años--;
meses += 12;
}
// «Ancla»: inicio avanzado exactamente años+meses correctos
const ancla = addMonths(
new Date(inicio.getFullYear() + años, inicio.getMonth(), inicio.getDate()),
meses
);
// Los días restantes son la diferencia exacta entre fin y el ancla
const dias = Math.round((fin - ancla) / (1000 * 60 * 60 * 24));
return { años, meses, dias };
}
Verificando el caso del 31 de enero → 1 de marzo de 2024:
años = 0,meses = 2fin.getDate()(1) <inicio.getDate()(31) →meses = 1ancla = addMonths(new Date(2024, 0, 31), 1)→ intenta Feb 31 → desborda → retrocede a Feb 29dias = (1 Mar − 29 Feb) = 1- Resultado: 0 años, 1 mes y 1 día ✓
Y el caso bisiesto → bisiesto: del 29 de febrero de 2020 al 29 de febrero de 2024:
años = 4,meses = 0fin.getDate()(29) ≥inicio.getDate()(29) → sin ajusteancla = addMonths(new Date(2024, 1, 29), 0)→ 29 Feb 2024dias = 0- Resultado: 4 años exactos ✓
Formatear el resultado en español
El castellano tiene singular y plural con nombres distintos para «mes», además del «año» que no cambia la raíz pero sí la terminación. Una función de ayuda evita los if dispersos:
function pluralizar(n, singular, plural) {
return n + ' ' + (n === 1 ? singular : plural);
}
function formatearDiferencia(años, meses, dias) {
const partes = [];
if (años > 0) partes.push(pluralizar(años, 'año', 'años'));
if (meses > 0) partes.push(pluralizar(meses, 'mes', 'meses'));
if (dias > 0 || partes.length === 0) partes.push(pluralizar(dias, 'día', 'días'));
if (partes.length === 1) return partes[0];
if (partes.length === 2) return partes.join(' y ');
return partes.slice(0, -1).join(', ') + ' y ' + partes[partes.length - 1];
}
La condición dias > 0 || partes.length === 0 garantiza que si todo es cero (misma fecha) se muestre «0 días» en lugar de una cadena vacía.
Integración en el formulario
Con inputs de tipo date la integración es directa. El evento change se dispara cada vez que el usuario selecciona una fecha, y basta con leer ambos valores para recalcular:
const inputInicio = document.getElementById('fecha-inicio');
const inputFin = document.getElementById('fecha-fin');
function actualizar() {
if (!inputInicio.value || !inputFin.value) return;
const inicio = new Date(inputInicio.value + 'T00:00:00');
const fin = new Date(inputFin.value + 'T00:00:00');
const total = Math.round((Math.max(fin, inicio) - Math.min(fin, inicio)) / 86400000);
const dif = calcularDiferencia(inicio, fin);
console.log(formatearDiferencia(dif.años, dif.meses, dif.dias));
console.log(total + ' días en total');
}
inputInicio.addEventListener('change', actualizar);
inputFin.addEventListener('change', actualizar);
A continuación puedes probar la herramienta completa: introduce las dos fechas y obtén al instante la diferencia en ambos formatos.
Tiempo entre dos fechas
Introduce las dos fechas y calcula la diferencia en años, meses y días, y en número total de días.


