Inicio / Artículos

Cinco trampas de la API temporal que reintroducen silenciosamente errores con fechas

Aprenda cómo la API Temporal de JavaScript aún puede causar errores relacionados con zonas horarias, serialización, precisión y duración, a menos que se corrijan cinco patrones comunes de uso incorrecto.

Editor: REACTAPP.TOP9 min de lectura1977 palabras

Fuentes: JavaScript Temporal API: 5 Critical Mistakes and How to Fix Them!

Una notificación de recordatorio diario que se activa una hora antes cada vez que cambian los horarios por el horario de verano. Un ticket de soporte registrado en el día calendario incorrecto porque el servidor funciona en UTC mientras que el cliente se encuentra en una zona horaria completamente distinta. Un informe financiero que intermitentemente falla en la comprobación de igualdad en CI, sin una forma clara de reproducir el fallo en una máquina local.

Ninguno de estos es un error causado por Date. Son errores temporales, y están apareciendo en bases de código que adoptaron Temporal precisamente para evitar los errores relacionados con las fechas desde un principio.

A partir de Node.js 26.8.2, Temporal se incluye de forma global sin necesidad de ninguna bandera experimental. Chrome 144 lo soporta por defecto. TC39 ha pasado la propuesta a la Etapa 4, lo que significa que ya no se trata de una adición futura especulativa: es ahora parte estable del lenguaje, y los equipos lo utilizan activamente para reemplazar a Moment.js, date-fns y las operaciones aritméticas personalizadas basadas en Date. Es precisamente en esa ola de migración donde suelen originarse los cinco errores mencionados a continuación.

Temporal resuelve los antiguos problemas de Date — la mutabilidad, el análisis ambiguo y la falta de un límite claro entre una fecha del calendario y un instante exacto en el tiempo — al imponer un modelo de datos más estricto y explícito. Los problemas surgen cuando los desarrolladores lo tratan simplemente como “un Date mejor”. Esa suposición es la que genera los errores descritos aquí, y estos suelen ser más sutiles que los antiguos, no más fáciles de detectar.

Error 1: Llamar a Temporal.Now.plainDateISO() sin decidir cuál “hoy” se quiere referir

Este es probablemente el error más común, sobre todo porque el código que lo causa parece completamente correcto a simple vista.

// Inside an API route running on a server in us-east-1
function getTodaysDigestDate() {
  const today = Temporal.Now.plainDateISO();
  return today.toString();
}

Al llamar a Temporal.Now.plainDateISO() sin argumentos, se utiliza la zona horaria del sistema para determinar la fecha actual, es decir, la zona en la que se ejecuta el servidor y no la del usuario final. Imagine a un cliente en Tokio que abre su aplicación a las 8 de la mañana, hora local: si el servidor está en UTC y aún no ha pasado la medianoche, la fecha registrada para ese cliente será la del día anterior. Este es el mismo problema de error por desfase que solía afectar a new Date().toDateString(); simplemente ha reaparecido en código del que los desarrolladores pensaban que estaba a salvo de él solo porque utiliza Temporal.

Un enfoque mejor:

function getTodaysDigestDate(userTimeZone) {
  // userTimeZone comes from the user's profile or resolved request context,
  // e.g. "Asia/Tokyo" — never the server's implicit zone.
  const today = Temporal.Now.plainDateISO(userTimeZone);
  return today.toString();
}

Temporal nunca eliminó la pregunta de “¿relativo a qué zona?”, solo obliga a responderla explícitamente en lugar de dejar que sea un valor por defecto silencioso. Cualquier llamada a Temporal.Now.* que omita una zona explícita representa una decisión que se tomó sin darse cuenta. Vale la pena buscar en su código base llamadas no calificadas a Temporal.Now.plainDateISO(), Temporal.Now.zonedDateTimeISO() y Temporal.Now.plainDateTimeISO(); cada una responde silenciosamente a “¿hoy, según quién?”, indicando “la zona en la que se encuentra ejecutando el servidor”.

Errore 2: Suponer que los viajes de ida y vuelta en JSON convierten un valor Temporal de nuevo en un objeto Temporal

Los objetos temporales exponen un método toJSON(), por lo que pasarlos a JSON.stringify() parece funcionar sin problemas; y ese éxito aparente es precisamente lo que genera el error siguiente.

const state = { renewalDate: Temporal.PlainDate.from("2026-11-03") };
localStorage.setItem("sub", JSON.stringify(state));
// ...later, on page reload...
const restored = JSON.parse(localStorage.getItem("sub"));
const daysLeft = restored.renewalDate.since(Temporal.Now.plainDateISO()).days;

JSON.stringify llama internamente a toJSON() y genera una cadena en formato ISO estándar. Pero JSON.parse no tiene forma de saber que esa cadena representaba anteriormente un Temporal.PlainDate; simplemente devuelve una cadena, no una instancia de clase. Ejecutar el fragmento anterior provoca el error TypeError: restored.renewalDate.since is not a function, y esto suele ocurrir en entornos de producción, solo después de que el valor haya pasado por algún tipo de proceso de serialización: localStorage, una llamada a postMessage hacia un Web Worker, un parámetro pasado a un Componente Servidor, o un almacén Redux persistente. En el desarrollo local, al trabajar dentro de la misma pestaña y en el mismo ciclo de ejecución, es fácil pasar por alto completamente este problema; solo aparece una vez que el valor realmente se serializa y luego se vuelve a leer.

Un enfoque mejor:

const restored = JSON.parse(localStorage.getItem("sub"));
const renewalDate = Temporal.PlainDate.from(restored.renewalDate);
const daysLeft = renewalDate.since(Temporal.Now.plainDateISO()).days;

La regla fundamental es la siguiente: cada tipo Temporal viene acompañado de un constructor estático .from() correspondiente para su reconstrucción, y cada vez que uno de estos valores pasa por una capa que lo convierte primero en texto plano o JSON —ya sea en el almacenamiento del navegador, un mensaje de worker, un payload de API o la transferencia entre servidor y cliente en un framework— usted es responsable de convertirlo nuevamente de forma explícita una vez que llegue a su destino. Nada realiza esa reconstrucción automáticamente, ni emite una advertencia cuando se omite ese paso.

Error 3: Olvidar que su base de datos no puede igualar la precisión de Temporal

Temporal.Instant mide el tiempo hasta el nivel del nanosegundo. Prácticamente ninguno de los sistemas a los que envía ese valor puede conservar ese nivel de detalle.

const capturedAt = Temporal.Now.instant(); // nanosecond precision
await db.query(
  "INSERT INTO events (captured_at) VALUES ($1)",
  [capturedAt.toString()]
);
// later, comparing the freshly captured instant to the one read back from Postgres
const row = await db.query("SELECT captured_at FROM events WHERE id = $1", [id]);
const stored = Temporal.Instant.from(row.captured_at);
console.log(capturedAt.equals(stored)); // false — intermittently, not always

El tipo de columna timestamptz de Postgres solo mantiene precisión hasta los microsegundos, mientras que un objeto Date de JavaScript solo almacena milisegundos. Al escribir directamente un valor Instant con precisión de nanosegundos en cualquiera de ellos, los dígitos adicionales se eliminan silenciosamente durante la inserción. La comparación mostrada anteriormente no es técnicamente incorrecta, pero mide un valor según un nivel de precisión que el almacenamiento en realidad nunca mantuvo. Si no se detecta, esto puede convertirse en pruebas que fallen intermitentemente, en verificaciones de deduplicación que permiten que ocasionalmente pase un elemento “duplicado”, o en registros de auditoría donde dos eventos que ocurrieron dentro del mismo microsegundo se vuelven indistinguibles sin que nadie lo haya pretendido.

Uso correcto:

const capturedAt = Temporal.Now.instant().round({ smallestUnit: "microsecond" });
await db.query(
  "INSERT INTO events (captured_at) VALUES ($1)",
  [capturedAt.toString()]
);
const row = await db.query("SELECT captured_at FROM events WHERE id = $1", [id]);
const stored = Temporal.Instant.from(row.captured_at);
console.log(capturedAt.equals(stored)); // true, consistently

Redondee el valor hacia abajo a la precisión que realmente soporte su capa de almacenamiento antes de escribirlo, en lugar de hacerlo después de haber perdido tiempo realizando comparaciones que solo fallan intermitentemente.

Errore 4: Persistir un instante fijo para un evento que aún no ha ocurrido

Este error es particularmente peligroso porque suele superar las pruebas y llegar a producción; el fallo solo se manifiesta dos veces al año.

function scheduleReminder(zone) {
  // "Remind me at 9 AM tomorrow" — computed once, at creation time
  const fireAt = Temporal.Now.zonedDateTimeISO(zone)
    .add({ days: 1 })
    .toInstant();
  return fireAt; // stored as-is, e.g. in a job queue's "run_at" column
}

Un Instant marca un único punto fijo en la línea de tiempo UTC; una vez que se convierte una hora local en uno, ya no “recuerda” que correspondía a las 9 a.m. hora local. Si la fecha programada cae después de un cambio de horario de verano con respecto al momento en que se creó el recordatorio, el desfase UTC de esa zona habrá cambiado, y la notificación terminará enviándose a las 8 a.m. o 10 a.m. hora local en lugar de a las 9. En el momento en que se ejecuta, no hay nada roto en el código; solo comete un error cuando cambian los relojes, lo cual es precisamente el tipo de defecto que se denomina “impredecible” y vuelve a surgir cada primavera y otoño.

Uso correcto:

function scheduleReminder(zone) {
  // Store the wall-clock intent, not a resolved instant
  const wallTime = Temporal.Now.plainDateTimeISO(zone).add({ days: 1 });
  return { wallTime: wallTime.toString(), timeZone: zone };
}
function resolveNextFireTime({ wallTime, timeZone }) {
  // Resolve to an actual Instant only when you're about to act on it
  return Temporal.PlainDateTime.from(wallTime)
    .toZonedDateTime(timeZone)
    .toInstant();
}

Cuando lo que realmente se quiere decir es “9 a.m. hora local, independientemente del desfase horario”, mantenga el valor correspondiente al reloj de pared junto con la zona horaria, y convierta ese valor en un Instant concreto solo en el último momento, justo antes de actuar sobre él, y no cuando se programó inicialmente.

Errore 5: Usar cálculos en milisegundos en lugar de Temporal.Duration

Los hábitos adquiridos con Date.getTime() no desaparecen automáticamente solo porque se haya pasado a un nuevo tipo.

function oneWeekLater(zonedDateTime) {
  const msPerWeek = 7 * 24 * 60 * 60 * 1000;
  const nextMs = zonedDateTime.epochMilliseconds + msPerWeek;
  return Temporal.Instant.fromEpochMilliseconds(nextMs)
    .toZonedDateTimeISO(zonedDateTime.timeZoneId);
}

Una semana calendárica no siempre suma exactamente 168 horas. Cuando el rango que se está calculando atraviesa un límite de cambio de hora, sumar milisegundos equivalentes a 7 * 24 horas a un valor de época produce una hora local incorrecta: una cita programada para “una semana después” se retrasa una hora, aunque la fecha en el calendario permanezca inalterada. Esto es esencialmente una variante menos evidente del Error 4, y tiende a reaparecer en cualquier función que siga basándose en cálculos brutos con milisegundos, simplemente porque así se hacían las operaciones matemáticas con Date en el pasado.

Uso correcto:

function oneWeekLater(zonedDateTime) {
  return zonedDateTime.add({ weeks: 1 });
}

Métodos como .add() y .subtract(), basados en Temporal.Duration, tienen en cuenta el calendario y las zonas horarias, por lo que “una semana después” se traduce correctamente al mismo tiempo según el reloj en el mismo día de la semana, incluyendo los horarios de verano. En cuanto comiences a calcular un desfase en milisegundos sin procesarlo adecuadamente y luego a convertirlo de nuevo, estarás saliendo del ámbito de la API Temporal diseñado para manejar esto correctamente.

Qué conecta a los cinco errores

Cada uno de estos problemas se remonta a la misma causa raíz: Temporal te obliga a ser explícito respecto al huso horario, la precisión y la intención, y cada error surge al recurrir silenciosamente a las suposiciones implícitas que utilizaba Date en su lugar. Temporal nunca estuvo pensado como un reemplazo mecánico; es un modelo más estricto que utiliza nombres de métodos que solo parecen familiares.

Resumen rápido:

  • Nunca invoques Temporal.Now.* sin decidir explícitamente qué “ahora” del huso horario necesitas realmente
  • Restaura los valores de Temporal con .from() en ambos lados de cualquier límite de serialización; JSON los convierte en cadenas simples, no en objetos vivos, durante el proceso de ida y vuelta
  • Redondee la precisión hacia abajo para que coincida con lo que su capa de almacenamiento puede retener realmente antes de guardarla, y no más tarde, al depurar una comprobación de igualdad que falla de forma impredecible
  • Para eventos con fecha futura, guarde la hora exacta junto con su zona horaria, y solo conviértala en un Instant justo cuando vaya a tomar acción al respecto
  • Prefiera la aritmética basada en Temporal.Duration — .add(), .subtract() — en lugar de cálculos basados en milisegundos desde la época, para cualquier operación que involucre fechas de calendario
  • Temporal realmente resuelve bien más de una década de problemas acumulados con Date. Pero intercambia ese desorden antiguo por un número menor de problemas más graves, y ahora que tanto Node 26 como Chrome 144 lo incluyen por defecto, más equipos se encontrarán directamente con esos problemas en producción a medida que realizan la migración.

    Lecturas relacionadas

  • Funcionalidades de Node.js 26 que reemplazan silenciosamente años de soluciones temporales — Un recorrido por la API Temporal de Node.js 26, la ejecución nativa de TypeScript, los ayudantes de caché y otras adiciones que eliminan soluciones temporales utilizadas desde hace tiempo.
  • Cinco errores engañosos de Node.js que pasan desapercibidos en las revisiones de código — Análisis de cinco errores reales en Node.js relacionados con forEach, promesas flotantes y copias superficiales para entender por qué el código que funciona bien puede fallar aún en producción.