Aprende a formatear y convertir tiempo en JavaScript sin sorpresas: timestamps, cadenas ISO, zonas horarias, horario de verano, reglas de parseo y patrones fiables.

Los errores relacionados con el tiempo en JavaScript rara vez se manifiestan como “el reloj está mal”. Aparecen como pequeños desplazamientos confusos: una fecha que es correcta en tu portátil pero incorrecta en la máquina de un compañero, una respuesta de la API que parece bien hasta que se muestra en otra zona horaria, o un informe que está “desfasado en uno” en torno a un cambio estacional.
Normalmente verás uno (o más) de estos:
+02:00) es distinto del que esperabas.Una gran fuente de problemas es que la palabra tiempo puede referirse a conceptos distintos:
"2025-12-23T10:00:00Z"). Esto es lo que normalmente quieres para logs, eventos y almacenamiento en APIs.El Date incorporado en JavaScript intenta cubrir todos estos casos, pero principalmente representa un instante en el tiempo y a la vez te empuja hacia la visualización local, lo que facilita conversiones accidentales.
Esta guía es intencionalmente práctica: cómo obtener conversiones previsibles entre navegadores y servidores, cómo elegir formatos más seguros (como ISO 8601) y cómo detectar las trampas clásicas (segundos vs milisegundos, UTC vs local y diferencias de parseo). El objetivo no es más teoría: es menos sorpresas de “¿por qué se desplazó?”.
Los errores de tiempo en JavaScript a menudo empiezan por mezclar representaciones que parecen intercambiables, pero no lo son.
1) Epoch en milisegundos (número)
Un número simple como 1735689600000 suele ser “milisegundos desde 1970-01-01T00:00:00Z”. Representa un instante en el tiempo sin formato ni zona horaria adjunta.
2) Objeto Date (envoltura de un instante)
Un Date almacena el mismo tipo de instante que un timestamp. La parte confusa: cuando imprimes un Date, JavaScript lo formatea usando las reglas locales del entorno a menos que pidas lo contrario.
3) Cadena formateada (para humanos)
Cadenas como "2025-01-01", "01/01/2025 10:00" o "2025-01-01T00:00:00Z" no son una sola cosa. Algunas son inequívocas (ISO 8601 con Z), otras dependen de la configuración regional y algunas no incluyen zona horaria.
El mismo instante puede mostrarse de forma distinta según la zona horaria:
const instant = new Date("2025-01-01T00:00:00Z");
instant.toLocaleString("en-US", { timeZone: "UTC" });
// "1/1/2025, 12:00:00 AM"
instant.toLocaleString("en-US", { timeZone: "America/Los_Angeles" });
// "12/31/2024, 4:00:00 PM" (día anterior)
Elige una única representación interna (comúnmente milisegundos epoch o ISO 8601 en UTC) y úsala de forma consistente en toda tu app y APIs. Convierte a/desde Date y cadenas formateadas solo en los límites: parseo de entrada y visualización en la interfaz.
Un “timestamp” suele significar tiempo epoch (también llamado tiempo Unix): el recuento de tiempo desde 1970-01-01 00:00:00 UTC. La trampa: distintos sistemas cuentan en distintas unidades.
El Date de JavaScript es la fuente de la mayoría de confusiones porque usa milisegundos. Muchas APIs, bases de datos y logs usan segundos.
17040672001704067200000Mismo momento, pero la versión en milisegundos tiene tres dígitos adicionales.
Usa multiplicación/división explícita para que la unidad sea obvia:
// seconds -> Date
const seconds = 1704067200;
const d1 = new Date(seconds * 1000);
// milliseconds -> Date
const ms = 1704067200000;
const d2 = new Date(ms);
// Date -> seconds
const secondsOut = Math.floor(d2.getTime() / 1000);
// Date -> milliseconds
const msOut = d2.getTime();
Date()Esto parece razonable, pero está mal cuando ts está en segundos:
const ts = 1704067200; // seconds
const d = new Date(ts); // WRONG: treated as milliseconds
El resultado será una fecha en 1970, porque 1,704,067,200 milisegundos son solo unos 19 días después del epoch.
Cuando no estés seguro de la unidad que tienes, añade guardas rápidas:
function asDateFromUnknownEpoch(x) {
// crude heuristic: seconds are ~1e9-1e10, milliseconds are ~1e12-1e13
if (x < 1e11) return new Date(x * 1000); // assume seconds
return new Date(x); // assume milliseconds
}
const input = Number(valueFromApi);
console.log({ input, digits: String(Math.trunc(input)).length });
console.log('as ISO:', asDateFromUnknownEpoch(input).toISOString());
Si el recuento de “dígitos” es ~10, probablemente son segundos. Si es ~13, probablemente son milisegundos. También imprime toISOString() mientras depuras: es inequívoco y te ayuda a detectar errores de unidad inmediatamente.
El Date de JavaScript puede ser confuso porque almacena un único instante en el tiempo, pero puede representar ese instante en distintas zonas horarias.
Internamente, un Date es esencialmente “milisegundos desde el epoch Unix (1970-01-01T00:00:00Z)”. Ese número representa un momento en UTC. El “desplazamiento” se produce cuando le pides a JavaScript formatear ese momento como hora local (según la configuración del equipo/servidor) frente a UTC.
Muchas APIs de Date tienen variantes locales y UTC. Devuelven números distintos para el mismo instante:
const d = new Date('2025-01-01T00:30:00Z');
d.getHours(); // hour in *local* time zone
d.getUTCHours(); // hour in UTC
d.toString(); // local time string
d.toISOString(); // UTC (always ends with Z)
Si tu máquina está en Nueva York (UTC-5), esa hora UTC puede aparecer como “19:30” del día anterior localmente. En un servidor configurado en UTC aparecerá como “00:30”. Mismo instante, diferente visualización.
Los logs a menudo usan Date#toString() o interpolan un Date implícitamente, lo que usa la zona horaria local del entorno. Eso significa que el mismo código puede imprimir timestamps distintos en tu portátil, en CI y en producción.
Almacena y transmite el tiempo como UTC (p. ej., milisegundos epoch o ISO 8601 con Z). Convierte a la zona del usuario solo al mostrar:
toISOString() o envía milisegundos epochIntl.DateTimeFormatSi estás construyendo una app rápidamente (por ejemplo con un flujo de trabajo de generación de código en Koder.ai), ayuda definir esto en tus contratos de API desde el principio: nombra campos claramente (createdAtMs, createdAtIso) y mantén el servidor (Go + PostgreSQL) y el cliente (React) consistentes en lo que representa cada campo.
Si necesitas enviar fechas/horas entre navegador, servidor y base de datos, las cadenas ISO 8601 son la opción por defecto más segura. Son explícitas, están muy soportadas y (lo más importante) llevan información de zona horaria.
Dos buenos formatos de intercambio:
2025-03-04T12:30:00Z2025-03-04T12:30:00+02:00¿Qué significa “Z”?
Z significa Zulu time, otro nombre para UTC. Así que 2025-03-04T12:30:00Z es “12:30 en UTC”.
¿Cuándo importan offsets como +02:00?
Los offsets son cruciales cuando un evento está ligado a un contexto de zona horaria local (citas, reservas, horarios de apertura). 2025-03-04T12:30:00+02:00 describe un momento que está dos horas por delante de UTC, y no es el mismo instante que 2025-03-04T12:30:00Z.
Cadenas como 03/04/2025 son una trampa: ¿es 4 de marzo o 3 de abril? Diferentes usuarios y entornos lo interpretan distinto. Prefiere 2025-03-04 (fecha ISO) o un datetime ISO completo.
const iso = "2025-03-04T12:30:00Z";
const d = new Date(iso);
const back = d.toISOString();
console.log(iso); // 2025-03-04T12:30:00Z
console.log(back); // 2025-03-04T12:30:00.000Z
Ese comportamiento de “round-trip” es exactamente lo que quieres para APIs: consistente, predecible y consciente de la zona horaria.
Date.parse() parece conveniente: le pasas una cadena y obtienes un timestamp. El problema es que, para cualquier cosa que no sea claramente ISO 8601, el parseo puede depender de heurísticas del navegador. Esas heurísticas han diferido entre motores y versiones, lo que significa que la misma entrada puede parsearse distinto (o no parsearse) según dónde se ejecute tu código.
Date.parse() puede variarJavaScript solo estandariza de forma fiable el parseo para cadenas estilo ISO 8601 (y aun así, detalles como la zona horaria importan). Para formatos “amigables”, como "03/04/2025", "March 4, 2025" o "2025-3-4", los navegadores pueden interpretar:
Si no puedes predecir la forma exacta de la cadena, no puedes predecir el resultado.
YYYY-MM-DDUna trampa común es el formulario de fecha simple "YYYY-MM-DD" (por ejemplo, "2025-01-15"). Muchos desarrolladores esperan que se interprete como medianoche local. En la práctica, algunos entornos tratan esta forma como medianoche UTC.
Esa diferencia importa: la medianoche UTC convertida a hora local puede convertirse en el día anterior en zonas con offset negativo (p. ej., América) o desplazar la hora inesperadamente. Es una forma fácil de obtener errores de “¿por qué mi fecha está desplazada un día?”.
Para entrada de servidor/API:
2025-01-15T13:45:00Z o 2025-01-15T13:45:00+02:00."YYYY-MM-DD") y evita convertirlo a Date salvo que también definas la zona horaria prevista.Para entrada de usuario:
03/04/2025 a menos que tu UI fuerce el significado.En lugar de confiar en Date.parse() para “resolverlo”, elige uno de estos patrones:
new Date(year, monthIndex, day) para fechas locales).Cuando los datos de tiempo son críticos, “parsea en mi máquina” no es suficiente: haz explícitas y consistentes tus reglas de parseo.
Si tu objetivo es “mostrar una fecha/hora de la manera que la gente espera”, la mejor herramienta en JavaScript es Intl.DateTimeFormat. Usa las reglas de localización del usuario (orden, separadores, nombres de meses) y evita el enfoque frágil de montar cadenas manualmente como month + '/' + day.
Formatear manualmente suele codificar por defecto el formato estadounidense, olvida ceros a la izquierda o produce resultados confusos de 24/12 horas. Intl.DateTimeFormat además hace explícita la zona horaria en la que estás mostrando—crítico cuando tus datos se almacenan en UTC pero la UI debe reflejar la hora local del usuario.
Para “formatearlo bien”, dateStyle y timeStyle son lo más sencillo:
const d = new Date('2025-01-05T16:30:00Z');
// User’s locale + user’s local time zone
console.log(new Intl.DateTimeFormat(undefined, {
dateStyle: 'medium',
timeStyle: 'short'
}).format(d));
// Force a specific time zone (great for event times)
console.log(new Intl.DateTimeFormat('en-GB', {
dateStyle: 'full',
timeStyle: 'short',
timeZone: 'UTC'
}).format(d));
Si necesitas ciclos horarios consistentes (p. ej., un ajuste en la configuración), usa hour12:
console.log(new Intl.DateTimeFormat('en-US', {
hour: 'numeric',
minute: '2-digit',
hour12: true
}).format(d));
Elige una función de formateo por cada “tipo” de timestamp en tu UI (hora de mensaje, entrada de log, inicio de evento) y mantén la decisión de timeZone intencional:
Esto te da una salida consistente y amigable con la localización sin mantener un conjunto frágil de cadenas de formato personalizadas.
El horario de verano (DST) es cuando una zona horaria cambia su offset respecto a UTC (típicamente una hora) en fechas específicas. Lo complicado: el DST no solo “cambia el offset”—cambia la existencia de ciertas horas locales.
Cuando los relojes adelantan, hay un rango de horas locales que nunca ocurre. Por ejemplo, en muchas regiones el reloj salta de 01:59 a 03:00, así que las 02:30 hora local están "ausentes".
Cuando los relojes atrasan, hay un rango de horas locales que ocurre dos veces. Por ejemplo, la 01:30 puede ocurrir una vez antes del cambio y otra vez después, lo que significa que la misma hora de reloj puede referirse a dos instantes distintos.
Estos no son equivalentes alrededor de los límites de DST:
Si el DST empieza esta noche, “mañana a las 9:00” podría estar a solo 23 horas de distancia. Si el DST termina esta noche, podría estar 25 horas de distancia.
// Scenario: schedule “same local time tomorrow”
const d = new Date(2025, 2, 8, 9, 0); // Mar 8, 9:00 local
const plus24h = new Date(d.getTime() + 24 * 60 * 60 * 1000);
const nextDaySameLocal = new Date(d);
nextDaySameLocal.setDate(d.getDate() + 1);
// Around DST, plus24h and nextDaySameLocal can differ by 1 hour.
setHours puede sorprenderteSi haces algo como date.setHours(2, 30, 0, 0) en un día de “adelanto de reloj”, JavaScript puede normalizarlo a una hora válida distinta (a menudo 03:30), porque 02:30 no existe en la hora local.
setDate) en lugar de sumar milisegundos.Z para que el instante sea inequívoco.Una fuente común de errores es usar Date para representar algo que no es un momento del calendario.
Un timestamp responde “¿cuándo pasó esto?” (un instante específico como 2025-12-23T10:00:00Z). Una duración responde “¿cuánto duró?” (como “3 minutos 12 segundos”). Son conceptos distintos, y mezclarlos conduce a cálculos confusos y efectos inesperados por zona horaria/DST.
Date es la herramienta equivocada para duracionesDate siempre representa un punto en la línea temporal relativo a un epoch. Si almacenas “90 segundos” como un Date, en realidad estás almacenando “1970-01-01 más 90 segundos” en una zona horaria concreta. Formatearlo puede mostrar 01:01:30, desplazarse una hora o mostrar una fecha que no pretendías.
Para duraciones, prefiere números sencillos:
HH:mm:ssAquí hay un formateador simple que funciona para temporizadores regresivos y duraciones de medios:
function formatHMS(totalSeconds) {
const s = Math.max(0, Math.floor(totalSeconds));
const hh = String(Math.floor(s / 3600)).padStart(2, "0");
const mm = String(Math.floor((s % 3600) / 60)).padStart(2, "0");
const ss = String(s % 60).padStart(2, "0");
return `${hh}:${mm}:${ss}`;
}
formatHMS(75); // "00:01:15" (countdown timer)
formatHMS(5423); // "01:30:23" (media duration)
Si conviertes desde minutos, multiplica primero (minutes * 60) y mantén el valor en forma numérica hasta que lo renders.
Cuando comparas tiempos en JavaScript, el enfoque más seguro es comparar números, no texto formateado. Un objeto Date es esencialmente una envoltura alrededor de un timestamp numérico (milisegundos epoch), así que quieres que las comparaciones acaben como “número vs número”.
Usa getTime() (o Date.valueOf(), que devuelve el mismo número) para comparar de forma fiable:
const a = new Date('2025-01-10T12:00:00Z');
const b = new Date('2025-01-10T12:00:01Z');
if (a.getTime() < b.getTime()) {
// a is earlier
}
// Also works:
if (+a < +b) {
// unary + calls valueOf()
}
Evita comparar cadenas formateadas como "1/10/2025, 12:00 PM": dependen de la localización y no ordenarán correctamente. La excepción principal son las cadenas ISO 8601 en el mismo formato y zona horaria (p. ej., todas ...Z), que son ordenables lexicográficamente.
Ordenar por tiempo es sencillo si ordenas por milisegundos epoch:
items.sort((x, y) => new Date(x.createdAt).getTime() - new Date(y.createdAt).getTime());
Filtrar elementos dentro de un rango es la misma idea:
const start = new Date('2025-01-01T00:00:00Z').getTime();
const end = new Date('2025-02-01T00:00:00Z').getTime();
const inRange = items.filter(i => {
const t = new Date(i.createdAt).getTime();
return t >= start && t < end;
});
El “inicio del día” depende de si te refieres a la hora local o a UTC:
// Local start/end of day
const d = new Date(2025, 0, 10); // Jan 10 in local time
const localStart = new Date(d.getFullYear(), d.getMonth(), d.getDate(), 0, 0, 0, 0);
const localEnd = new Date(d.getFullYear(), d.getMonth(), d.getDate(), 23, 59, 59, 999);
// UTC start/end of day
const utcStart = new Date(Date.UTC(2025, 0, 10, 0, 0, 0, 0));
const utcEnd = new Date(Date.UTC(2025, 0, 10, 23, 59, 59, 999));
Elige una definición pronto y mantenla consistentemente en tus comparaciones y lógica de rangos.
Los bugs de tiempo parecen aleatorios hasta que identificas qué tienes (timestamp? cadena? Date?) y dónde se introduce el desplazamiento (parseo, conversión de zona horaria, formateo).
Empieza registrando el mismo valor de tres formas distintas. Esto revela rápidamente si el problema es segundos vs milisegundos, local vs UTC o parseo de cadenas.
console.log('raw input:', input);
const d = new Date(input);
console.log('toISOString (UTC):', d.toISOString());
console.log('toString (local):', d.toString());
console.log('timezone offset (min):', d.getTimezoneOffset());
Qué buscar:
toISOString() está muy errada (p. ej., año 1970 o un futuro lejano), sospecha segundos vs milisegundos.toISOString() parece correcta pero toString() está “desplazada”, estás viendo un problema de visualización en zona horaria local.getTimezoneOffset() cambia según la fecha, estás cruzando horario de verano.Muchas veces de “funciona en mi máquina” son simplemente distintos valores por defecto de entorno.
console.log(Intl.DateTimeFormat().resolvedOptions());
console.log('TZ:', process.env.TZ);
console.log(Intl.DateTimeFormat().resolvedOptions().timeZone);
Si tu servidor corre en UTC pero tu portátil en una zona local, la salida formateada diferirá a menos que especifiques timeZone explícitamente.
Crea tests unitarios alrededor de los límites de DST y tiempos “borde”:
23:30 → 00:30Si iteras rápido, considera incluir estos tests en tu scaffolding. Por ejemplo, cuando generas una app React + Go en Koder.ai, puedes añadir una pequeña suite de “contratos de tiempo” desde el inicio (ejemplos de payloads de API + aserciones de parseo/formateo) para que las regresiones se detecten antes del despliegue.
"2025-03-02 10:00".locale y (cuando hace falta) timeZone.El manejo fiable del tiempo en JavaScript consiste en elegir una “fuente de verdad” y ser consistente desde el almacenamiento hasta la visualización.
Almacena y calcula en UTC. Trata la hora local del usuario como un detalle de presentación.
Transmite fechas entre sistemas como cadenas ISO 8601 con un offset explícito (preferiblemente Z). Si debes enviar epochs numéricos, documenta la unidad y manténla consistente (milisegundos es el defecto común en JS).
Formatea para humanos con Intl.DateTimeFormat (o toLocaleString), y pasa timeZone explícito cuando necesites salida determinista (por ejemplo, mostrar siempre en UTC o en una región de negocio específica).
Z (p. ej., 2025-12-23T10:15:00Z). Si usas epochs, incluye un nombre de campo como createdAtMs para dejar claro la unidad.Considera una librería dedicada si necesitas eventos recurrentes, reglas complejas de zonas horarias, aritmética segura respecto a DST (“misma hora local mañana”) o mucho parseo de entradas inconsistentes. El valor está en APIs más claras y menos bugs en casos límite.
Si quieres profundizar, explora más guías relacionadas con el tiempo en /blog. Si estás evaluando herramientas u opciones de soporte, consulta /pricing.