Cinq pièges de l’API Temporal qui réintroduisent silencieusement des erreurs de dates
Découvrez comment l’API Temporal de JavaScript peut encore provoquer des problèmes liés aux fuseaux horaires, à la sérialisation, à la précision et à la durée, à moins que cinq modes d’usage incorrects courants ne soient corrigés.
Sources: JavaScript Temporal API: 5 Critical Mistakes and How to Fix Them!
Une notification de rappel quotidien qui s’active une heure plus tôt chaque fois que les horaires changent en raison de l’heure d’été. Un ticket de support enregistré sous le mauvais jour du calendrier, car le serveur fonctionne en UTC tandis que le client se trouve dans une zone complètement différente. Un rapport financier qui échoue de manière intermittente au test d’égalité dans les outils CI, sans moyen clair de reproduire cette erreur sur une machine locale.
Aucun de ces problèmes n’est causé par Date. Ce sont des bugs temporels, et ils apparaissent dans des bases de code qui ont adopté Temporal justement pour éviter les bugs liés aux dates.
Dès Node.js 26.8.2, Temporal est inclus par défaut dans toutes les versions sans nécessiter de flag expérimental. Chrome 144 le prend en charge nativement. Le TC39 a déplacé cette proposition en phase 4, ce qui signifie qu’il ne s’agit plus d’une fonctionnalité future spéculative — c’est désormais une partie intégrante du langage, et les équipes l’utilisent activement pour remplacer Moment.js, date-fns ainsi que les calculs personnalisés basés sur Date. C’est précisément dans le cadre de cette vague de migration que les cinq erreurs ci-dessous ont tendance à apparaître.
Temporal résout les anciens problèmes liés à Date — la mutabilité, l’interprétation ambiguë, ainsi que l’absence de frontière claire entre une date calendaire et un instant précis dans le temps — en imposant un modèle de données plus strict et plus explicite. Les problèmes commencent lorsque les développeurs le considèrent simplement comme « une version améliorée de Date ». C’est cette supposition qui entraîne les erreurs décrites ici, et celles-ci ont tendance à être plus insidieuses que les anciennes, moins faciles à détecter.
Erreur 1 : Appeler Temporal.Now.plainDateISO() sans déterminer quel « aujourd’hui » on entend
C’est probablement l’erreur la plus fréquente, principalement parce que le code qui la provoque semble entièrement correct à première vue.
// Inside an API route running on a server in us-east-1
function getTodaysDigestDate() {
const today = Temporal.Now.plainDateISO();
return today.toString();
}
L’appel de Temporal.Now.plainDateISO() sans arguments utilise la zone horaire du système pour déterminer l’heure « maintenant » — c’est-à-dire la zone dans laquelle le serveur est en cours d’exécution, et non celle de l’utilisateur final. Imaginez un client à Tokyo qui ouvre votre application à 8 heures du matin, heure locale : si le serveur est en UTC et qu’il n’a pas encore dépassé minuit, la date affichée pour ce client correspondra à celle d’hier. Il s’agit du même problème de décalage d’une unité qui affectait auparavant new Date().toDateString() — il réapparaît simplement dans du code que les développeurs pensaient à l’abri de ce problème uniquement parce qu’il utilise Temporal.
Une approche meilleure :
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 n’a jamais résolu la question « par rapport à quelle zone ? » — il vous oblige simplement à y répondre explicitement au lieu de laisser le système choisir silencieusement. Toute appel à Temporal.Now.* omettant une zone spécifique correspond à une décision que vous avez prise sans vous en rendre compte. Il est utile de rechercher dans votre codebase les appels non qualifiés à Temporal.Now.plainDateISO(), Temporal.Now.zonedDateTimeISO() et Temporal.Now.plainDateTimeISO() — chacun d’eux répond silencieusement à « aujourd’hui, selon qui ? » par « la zone dans laquelle le serveur est actuellement en cours d’exécution ».
Erreure 2 : Supposer que les échanges JSON transforment une valeur Temporal en un objet Temporal
Les objets temporaires exposent une méthode toJSON(), de sorte que leur transmission à JSON.stringify() semble fonctionner sans problème — et c’est précisément ce succès apparent qui entraîne le bug suivant.
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 appelle en réalité toJSON() et génère une chaîne au format ISO standard. Cependant, JSON.parse ne peut pas savoir que cette chaîne représentait auparavant un Temporal.PlainDate — il renvoie simplement une chaîne, et non une instance de classe. L’exécution du code ci-dessus provoque l’erreur TypeError: restored.renewalDate.since is not a function, ce qui se produit généralement en environnement de production, après que la valeur ait traversé une sorte de limite de sérialisation : localStorage, une appelation postMessage vers un Web Worker, une propriété transmise à un composant serveur, ou un stock Redux persistant. Lors du développement local, en travaillant dans la même fenêtre et au cours de la même exécution, ce problème est facile à manquer complètement — il n’apparaît que lorsque la valeur a réellement été sérialisée puis lu à nouveau.
Une approche plus efficace :
const restored = JSON.parse(localStorage.getItem("sub"));
const renewalDate = Temporal.PlainDate.from(restored.renewalDate);
const daysLeft = renewalDate.since(Temporal.Now.plainDateISO()).days;
La règle de base est la suivante : chaque type Temporal dispose d’un constructeur statique .from() correspondant pour sa reconstruction, et chaque fois que l’un de ces valeurs passe par une couche qui la transforme d’abord en texte brut ou en JSON — que ce soit dans le stockage du navigateur, un message de travailleur, une charge utile API, ou lors du transfert entre serveur et client dans un framework — c’est à vous de la convertir explicitement une fois qu’elle est arrivée à destination. Rien ne effectue cette reconstruction automatiquement pour vous, et rien n’émet d’avertissement lorsque cette étape est omise.
Erreur 3 : Oublier que votre base de données ne peut pas atteindre la précision de Temporal
Temporal.Instant mesure le temps jusqu’au nanoseconde. Pratiquement aucun des systèmes auxquels vous transmettez cette valeur ne peut conserver un tel niveau de détail.
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
Le type de colonne timestamptz de Postgres ne conserve qu’une précision au niveau du microseconde, tandis qu’un objet Date JavaScript classique ne gère que les millisecondes. Lorsqu’on écrit directement un objet Instant avec précision au nanoseconde dans l’un ou l’autre, les chiffres supplémentaires sont simplement supprimés lors de l’insertion. La comparaison présentée dans l’extrait ci-dessus n’est pas techniquement fausse — elle mesure simplement une valeur par rapport à un niveau de précision que le stockage n’a jamais réellement conservé. Si cela reste inaperçu, cela peut entraîner des tests qui échouent de manière intermittente, une vérification de déduplication qui laisse passer occasionnellement des éléments considérés comme « dupliqués », ou un journal d’audit où deux événements survenus au même microseconde deviennent indiscernables, sans que personne ne l’ait voulu.
Utilisation correcte :
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
Arrondissez la valeur vers le bas à la précision réellement supportée par votre couche de stockage avant de l’écrire, plutôt que d’attendre après avoir perdu du temps à effectuer des comparaisons qui échouent uniquement de manière intermittente.
Erreur 4 : Stocker un instant fixe pour un événement qui n’a pas encore eu lieu
Cette erreur est particulièrement dangereuse car elle passe généralement les tests et atteint la production — l’échec ne se manifeste qu’une fois par an.
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
}
Instant représente un point unique et fixe sur la chronologie UTC ; une fois qu’une heure locale a été convertie en tel instant, il ne « se souvient » plus qu’il s’agissait de 9 heures du matin, heure locale. Si la date prévue tombe après un changement d’heure d’été par rapport au moment où le rappel a été créé, l’offset UTC de cette zone aura changé, et la notification sera déclenchée à 8 heures ou 10 heures du matin, heure locale, plutôt qu’à 9 heures. Rien dans le code n’est défectueux au moment de son exécution — il ne devient incorrect que lorsque les horloges changent, ce qui est précisément le type de défaut qualifié de « instable » et qui réapparaît chaque printemps et automne.
Utilisation correcte :
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();
}
Lorsque ce que vous voulez vraiment dire est « 9 h du matin, heure locale, quel que soit l’offset », conservez la valeur indiquée par une horloge murale accompagnée de la zone horaire, et ne convertissez-la en un Instant concret qu’au dernier moment — juste avant d’en faire usage, et non au moment où elle a été planifiée.
Erreur 5 : Utiliser des calculs en millisecondes au lieu de Temporal.Duration
Les habitudes héritées de Date.getTime() ne disparaissent pas automatiquement simplement parce que vous avez changé de type.
function oneWeekLater(zonedDateTime) {
const msPerWeek = 7 * 24 * 60 * 60 * 1000;
const nextMs = zonedDateTime.epochMilliseconds + msPerWeek;
return Temporal.Instant.fromEpochMilliseconds(nextMs)
.toZonedDateTimeISO(zonedDateTime.timeZoneId);
}
Une semaine calendaire ne compte pas toujours exactement 168 heures. Lorsque la période que vous calculez traverse une frontière de changement d’heure d’été, l’ajout de millisecondes équivalant à 7 * 24 heures à une valeur d’époque produit une heure locale incorrecte — un rendez-vous censé avoir lieu « une semaine plus tard » est décalé d’une heure, même si la date calendaire elle-même reste inchangée. Il s’agit essentiellement d’une variante plus discrète de l’Erreur 4, et ce problème réapparaît souvent dans toute fonction encore basée sur des calculs en millisecondes brutes, simplement parce c’est ainsi que les calculs de date étaient effectués auparavant.
Utilisation correcte :
function oneWeekLater(zonedDateTime) {
return zonedDateTime.add({ weeks: 1 });
}
Des méthodes telles que .add() et .subtract(), basées sur Temporal.Duration, tiennent compte du calendrier et des zones horaires, de sorte que « une semaine plus tard » correspond correctement à la même heure indiquée par l’horloge murale le même jour de la semaine, y compris en tenant compte des changements d’heure. Dès que vous commencez à calculer un décalage en millisecondes brut et à le convertir ultérieurement, vous sortez du cadre de l’API Temporal conçue pour gérer cela correctement.
Ce qui relie les cinq erreurs
Tous ces problèmes trouvent leur origine dans la même cause fondamentale : Temporal vous oblige à être explicite quant à la zone horaire, à la précision et à l’intention, et chaque erreur provient du recours silencieux aux hypothèses implicites que Date utilisait à la place. Temporal n’a jamais été conçu comme un simple remplaçant mécanique — c’est un modèle plus strict utilisant des noms de méthodes qui ne sont que superficiellement familiers.
Rappel rapide :
- N’appellez jamais
Temporal.Now.*sans décider explicitement quelle « maintenant » de zone horaire vous avez réellement besoin - Restaurer les valeurs Temporal avec
.from()des deux côtés de toute frontière de sérialisation — JSON les transforme en simples chaînes de caractères, et non en objets actifs, lors du trajet aller-retour
Instant qu’au moment où vous comptez y agirTemporal.Duration — .add(), .subtract() — plutôt que les calculs en millisecondes depuis l’époque commune pour tout ce qui concerne les dates calendairesTemporal résout véritablement bien plus d’une décennie de problèmes accumulés avec Date. Cependant, il remplace ce chaos ancien par un nombre moindre de problèmes plus aigus ; et maintenant que Node 26 ainsi que Chrome 144 le prennent en charge par défaut, de plus en plus d’équipes vont se heurter directement à ces problèmes en production lors de leur migration.