"¿Por qué mi reunión de las 9:00 sale a las 8:00?" "El informe de ayer incluye datos de hoy." Los bugs de fechas son tan universales porque parecen triviales y no lo son: detrás de una fecha hay física (la Tierra gira mal), política (los gobiernos cambian las reglas del DST) e historia (offsets con segundos y minutos). Este artículo da el modelo mental correcto y las reglas prácticas que eliminan el 95% de estos bugs.
El modelo mental: instante ≠ representación
Todo empieza separando dos conceptos que el lenguaje cotidiano mezcla:
- Instante: un punto absoluto en la línea temporal. "El momento en que ocurrió". No tiene zona horaria.
- Representación civil: cómo un humano de un lugar concreto etiqueta ese instante. "9 de marzo a las 9:00 en Madrid".
La regla de oro única de este artículo: almacena y transmite instantes; convierte a representación solo al mostrar.
Un instante se representa canónicamente como UTC o como Unix timestamp:
2026-08-24T07:00:00Z ← ISO 8601, la Z significa UTC
1756018800 ← segundos desde 1970-01-01T00:00:00Z
Ambos identifican el mismo segundo exacto sin ambigüedad posible.
El error fundacional: guardar horas locales
La aplicación que guarda "2026-08-24 09:00" sin zona ha perdido información para siempre. Cuando otro usuario —o el mismo tras volar— consulta ese dato, ¿09:00 de dónde? Las consecuencias típicas:
- Servidores en UTC y usuarios en España: todo desplazado 1-2 horas según estación.
- Datos duplicados o huecos en informes diarios cuando el DST mueve el reloj.
- Comparaciones entre registros de distintos orígenes que no cuadran nunca.
Y el caso trampa: funciona perfecto durante meses... hasta el cambio horario de marzo u octubre, cuando el offset salta. Es el bug que aparece dos veces al año y siempre en producción.
Offset ≠ zona horaria
Distinción que resuelve media confusión: +02:00 es un offset, un número. Europe/Madrid es una zona horaria: una regla histórica y futura que dice qué offset aplica en cada fecha.
Por qué importa: España en verano es +02:00 y en invierno +01:00. Si guardas el offset fijo en vez de la zona, tu dato queda congelado en el verano eterno. Además las zonas incorporan decisiones políticas: España vivió el horario solar hasta 1940 y se saltó una hora de madrugada al alinearse con CET. La base de datos IANA (tzdata) mantiene toda esa historia por región — y se actualiza varias veces al año porque los gobiernos siguen cambiando reglas.
Reglas derivadas:
- Nunca calcules offsets a mano ni los hardcodees.
- Guarda identificadores IANA (
Europe/Madrid,America/New_York), no abreviaturas ambiguas (CST significa tres cosas distintas según el país). - Mantén
tzdataactualizada en servidor y sistema operativo.
DST: la hora que existe dos veces (y la que no existe)
Los cambios de hora crean dos anomalías matemáticas que rompen aritmética ingenua:
En otoño (reloj atrás): las 02:30 ocurre dos veces — primero con +02:00, luego con +01:00. Sumar "una hora" a las 02:15 puede devolverte las mismas 02:15.
En primavera (reloj adelante): las 02:30 no existe. Un usuario introduciendo esa hora produce un instante indefinido que cada librería resuelve distinto.
Consecuencia práctica: nunca hagas aritmética de días sumando 86.400 segundos. "Mañana" se calcula sobre calendario, no sobre duración:
// MAL: falla en cambios de DST
const manana = new Date(fecha.getTime() + 86400_000);
// BIEN: aritmética de calendario
const manana = new Date(fecha);
manana.setDate(manana.getDate() + 1);
JavaScript: lo mínimo imprescindible
El Date nativo almacena internamente un timestamp UTC (milisegundos) — el modelo correcto — pero sus métodos locales (getHours(), toLocaleString()) dependen de la zona del dispositivo, fuente de sorpresas en servidores (que suelen correr en UTC) versus usuarios.
Para formatear en zona específica, la API Intl moderna:
new Date("2026-08-24T07:00:00Z").toLocaleString("es-ES", {
timeZone: "Europe/Madrid",
dateStyle: "long",
timeStyle: "short"
});
// "24 de agosto de 2026, 9:00"
Y desde 2025 los navegadores soportan Temporal, la API que corrige décadas de defectos: tipos separados para instante (Temporal.Instant), fecha sin hora (PlainDate) y datetime con zona (ZonedDateTime). En proyectos nuevos donde el soporte alcance, es la opción seria.
Para convertir y verificar timestamps concretos, nuestro convertidor de Unix timestamp muestra el mismo instante en tu zona local, en UTC y su valor numérico bidireccionalmente.
Checklist anti-bugs
- Base de datos: columnas
timestamptz(Postgres) o timestamps UTC puros; jamásdatetimelocal sin zona. - APIs: intercambio exclusivamente en ISO 8601 con
Z. - Frontend: convertir a zona del usuario solo en render; recalcular si cambia su zona.
- Recordatorios/agendas: guardar la hora civil + zona IANA ("09:00 Europe/Madrid"), porque "las 9 de la mañana" debe seguir siendo las 9 aunque el offset cambie.
- Tests: cubrir siempre el día del cambio de hora (marzo/octubre) y años bisiestos.
- Logs: siempre UTC; el debugging te lo agradecerá.
Preguntas frecuentes
¿Debo pedir al usuario su zona horaria? Para mostrar datos, usa la detectada automáticamente (Intl.DateTimeFormat().resolvedOptions().timeZone) con posibilidad de cambiarla en ajustes. Para eventos programados, pregúntala explícitamente: la del portátil puede no ser la relevante.
¿Unix timestamp tiene problema del año 2038? El clásico de 32 bits con signo se agota en enero de 2038. Cualquier sistema moderno con enteros de 64 bits (todos los runtimes web actuales) está fuera de peligro.
¿Qué pasa si España suprime el cambio de hora? Exactamente lo previsto por el diseño correcto: actualizas tzdata, y las aplicaciones que guardaron zonas IANA siguen funcionando; las que hardcodearon offsets, no.
Convierte y verifica timestamps con el convertidor de Unix Timestamp, gratis y directamente en tu navegador.