Startseite / Artikel

Fünf temporale API-Fallen, die heimlich Datenfehler wieder einführen

Erfahren Sie, wie die Temporal-API von JavaScript trotzdem zu Fehlern bei Zeitzone, Serialisierung, Präzision und Dauer führen kann, sofern fünf häufige Fehlgebrauchsmuster nicht korrigiert werden.

Verlag: REACTAPP.TOP9 Min. Lesezeit1977 Wörter

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

Eine tägliche Erinnerungsnachricht, die jeweils eine Stunde früher ausgelöst wird, sobald die Uhren wegen der Sommerzeit angepasst werden. Ein Support-Ticket, das unter dem falschen Kalendertag erfasst wird, weil der Server in UTC läuft, während der Kunde sich in einer völlig anderen Zeitzone befindet. Ein Finanzbericht, der in der CI-Testsuite gelegentlich die Gleichheitsprüfung nicht besteht, wobei es keine klare Möglichkeit gibt, den Fehler auf einem lokalen Rechner nachzustellen.

Niemand von diesen Problemen ist ein durch Date verursachter Fehler. Es handelt sich um temporale Fehler, und sie tauchen in Codebasen auf, die Temporal gerade deshalb übernommen haben, um von ursprünglich mit Datumsangaben verbundenen Fehlern wegzukommen.

Ab Node.js 26.8.2 wird Temporal weltweit ohne erforderliches experimentelles Flag ausgeliefert. Chrome 144 unterstützt es bereits standardmäßig. TC39 hat den Vorschlag in Phase 4 versetzt, was bedeutet, dass es nicht mehr als spekulative zukünftige Erweiterung gilt – es ist nun ein fester Bestandteil der Sprache, und Teams nutzen es aktiv, um Moment.js, date-fns sowie benutzerdefinierte Date-basierte Berechnungen zu ersetzen. Genau in diesem Migrationsprozess entstehen häufig die fünf unten aufgeführten Fehler.

Temporal beseitigt die alten Probleme von Date – Veränderbarkeit, unklare Parsing-Verfahren sowie das Fehlen einer klaren Trennlinie zwischen einem Kalendertag und einem exakten Zeitpunkt – indem es ein strengeres, ausdrücklicheres Datenmodell vorschreibt. Die Probleme entstehen, wenn Entwickler Temporal lediglich als „eine verbesserte Date“ betrachten. Genau diese Annahme führt zu den hier beschriebenen Fehlern, und diese neigen dazu, heimtückischer zu sein als die alten, sodass sie schwieriger aufzudecken sind.

Fehler 1: Aufruf von Temporal.Now.plainDateISO() ohne Festlegung, welcher „Heute“ gemeint ist

Das ist vermutlich der häufigste Fehler überhaupt – hauptsächlich, weil der Code, der ihn verursacht, auf den ersten Blick völlig korrekt erscheint.

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

Die Aufrufung von Temporal.Now.plainDateISO() ohne Argumente verwendet die Zeitzone des *Systems* – also die Zone, in der der Server läuft, und nicht die Zone des Endbenutzers – um „jetzt“ zu bestimmen. Stellen Sie sich einen Kunden in Tokio vor, der Ihre App um 8 Uhr morgens Ortszeit öffnet: Wenn der Server in UTC läuft und noch nicht nach Mitternacht gekommen ist, wird das Datum dieses Kunden mit dem gestrigen Datum versehen. Dies ist dasselbe Problem des Fehlers um eins, das früher new Date().toDateString() plagte – es taucht einfach wieder in Code auf, von dem Entwickler annahmen, er sei davon verschont, nur weil Temporal verwendet wird.

Eine bessere Vorgehensweise:

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 beseitigt niemals die Frage „In Bezug auf welche Zone?“ – es zwingt einen lediglich dazu, diese explizit zu beantworten, anstatt sie stillschweigend als Standardwert zu verwenden. Jeder Aufruf von Temporal.Now.*, bei dem eine explizite Zone weggelassen wird, ist eine Entscheidung, die man unbewusst getroffen hat. Es lohnt sich, in der Codebasis nach unqualifizierten Aufrufen von Temporal.Now.plainDateISO(), Temporal.Now.zonedDateTimeISO() und Temporal.Now.plainDateTimeISO() zu suchen – jeder dieser Aufrufe beantwortet stillschweigend die Frage „Heute, nach wem?“ mit „der Zone, in der der Server zufällig läuft.“

Fehler 2: Annahme, dass JSON-Übertragungen einen Temporal-Wert wieder in ein Temporal-Objekt umwandeln

Temporäre Objekte bieten eine toJSON()-Methode bereit, wodurch ihr Übergeben an JSON.stringify() nahtlos zu funktionieren scheint – und genau dieser scheinbare Erfolg ist es, der den folgenden Fehler verursacht.

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 ruft im Hintergrund toJSON() auf und erzeugt einen einfachen, in ISO-Format vorliegenden String. Doch JSON.parse hat keine Möglichkeit, herauszufinden, dass dieser String ursprünglich einen Temporal.PlainDate darstellte – es gibt lediglich einen String zurück, keine Klasseninstanz. Die Ausführung des obigen Snippets führt zu dem Fehler TypeError: restored.renewalDate.since ist keine Funktion, und das geschieht in der Regel in der Produktion, erst nachdem der Wert durch eine Art Serialisierungsbarriere gegangen ist: localStorage, ein postMessage-Aufruf an einen Web Worker, ein Prop, der an ein Server Component übergeben wird, oder ein persistenter Redux-Store. Bei lokaler Entwicklung, innerhalb derselben Tab und im selben Ausführungsschritt, fällt dieses Problem leicht überhaupt nicht auf – es tritt erst dann auf, wenn der Wert tatsächlich serialisiert und später wieder ausgelesen wird.

Ein besseres Vorgehen:

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

Die zugrundeliegende Regel lautet wie folgt: Jeder Temporal-Typ wird mit einem entsprechenden .from()-Statischkonstruktor zur Rekonstruktion mitgeliefert. Immer dann, wenn eines dieser Werte zunächst durch eine Schicht geleitet wird, die ihn in reinen Text oder JSON umwandelt – sei es der Browser-Speicher, eine Worker-Nachricht, ein API-Payload oder die Übertragung zwischen Server und Client in einem Framework – sind Sie dafür verantwortlich, ihn nach seiner Ankunft explizit wieder umzuwandeln. Nichts führt diese Rekonstruktion automatisch für Sie durch, und nichts gibt einen Warnhinweis ab, wenn dieser Schritt übersprungen wird.

Fehler 3: Vergessen, dass Ihre Datenbank die Präzision von Temporal nicht erreichen kann

Temporal.Instant misst die Zeit bis auf den Nanosekundenbruchteil genau. Praktisch keines der Systeme, an die Sie diesen Wert weiterleiten, kann dieses Detailniveau beibehalten.

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

Der timestamptz-Spaltentyp von Postgres behält nur eine Präzision bis zu Mikrosekunden, während ein gewöhnlicher JavaScript-Date-Objekt nur Millisekunden speichert. Wenn man ein Instant-Objekt mit Nanosekundenpräzision direkt in einen dieser Typen schreibt, werden die zusätzlichen Ziffern beim Einfügen stillschweigend abgeschnitten. Der Vergleich im obigen Beispiel ist technisch nicht falsch – er misst lediglich einen Wert anhand eines Präzisionsniveaus, das der Speicher eigentlich nie beibehält. Wenn dies unentdeckt bleibt, führt das zu intermittierend fehlerhaften Tests, einer Deduplizierungsprüfung, bei der gelegentlich „Doppelte“ durchkommen, oder zu einer Audit-Trail, in der zwei Ereignisse, die innerhalb derselben Mikrosekunde stattfinden, ohne dass jemand dieses Ergebnis beabsichtigt, nicht mehr voneinander zu unterscheiden sind.

Korrekte Verwendung:

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

Runden Sie den Wert auf die Genauigkeit herunter, die Ihre Speicheschicht tatsächlich unterstützt bevor Sie ihn speichern, anstatt erst nachdem Sie Zeit mit Vergleichen verschwendet haben, die nur gelegentlich fehlschlagen.

Fehler 4: Speichern eines festen Zeitpunkts für ein Ereignis, das noch nicht eingetreten ist

Dieser Fehler ist besonders gefährlich, weil er in der Regel die Tests besteht und in die Produktion gelangt – das Versagen tritt erst zweimal im Jahr auf.

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
}

Ein Instant markiert einen einzigen, festen Zeitpunkt auf der UTC-Zeitlinie; sobald eine lokale Uhrzeit in einen Instant umgewandelt wurde, „erinnert“ er sich nicht mehr daran, dass es 9 Uhr morgens Ortszeit bedeutet hat. Wenn das geplante Datum nach einer Sommerzeitumstellung relativ zu dem Zeitpunkt liegt, an dem die Erinnerung erstellt wurde, hat sich der UTC-Offset für diese Zeitzone geändert, wodurch die Benachrichtigung stattdessen um 8 oder 10 Uhr morgens Ortszeit statt um 9 Uhr ausgelöst wird. Im Moment der Ausführung ist mit dem Code alles in Ordnung – er funktioniert nur falsch, sobald die Uhren umgestellt werden, was genau die Art von Defekt ist, der als „unzuverlässig“ bezeichnet wird und jedes Frühjahr sowie Herbst wieder auftaucht.

Richtige Verwendung:

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();
}

Falls Sie eigentlich „9 Uhr morgens Ortszeit, unabhängig von der Zeitzone“ meinen, sollten Sie den Wert der Wanduhr zusammen mit der Zeitzone beibehalten und diesen erst in letzter Minute – direkt bevor Sie darauf zugreifen – in einen konkreten Instant umwandeln, und nicht bereits zu dem Zeitpunkt, an dem der Termin festgelegt wurde.

Fehler 5: Verwendung von Millisekundenrechnungen anstelle von Temporal.Duration

Gewohnheiten aus Date.getTime() verschwinden nicht automatisch, nur weil man auf einen neuen Typ umgestiegen ist.

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

Eine Kalenderwoche umfasst nicht immer genau 168 Stunden. Wenn der zu berechnende Zeitraum eine Sommerzeitgrenze überschreitet, führt das Hinzufügen von Millisekunden im Wert von 7 * 24 Stunden zu einem falschen lokalen Uhrzeitwert – ein Termin, der eigentlich „eine Woche später“ stattfinden soll, verschiebt sich um eine Stunde, obwohl das Kalendertag selbst unverändert bleibt. Dies ist im Grunde eine mildere Variante von Fehler 4 und tritt oft wieder auf in Funktionen, die weiterhin auf reiner Millisekundenrechnung basieren, einfach weil früher die Date-Mathematik auf diese Weise durchgeführt wurde.

Richtige Verwendung:

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

Methoden wie .add() und .subtract(), die auf Temporal.Duration basieren, berücksichtigen Kalender und Zeitzone, sodass „eine Woche später“ korrekt zur gleichen Uhrzeit am selben Wochentag – auch unter Berücksichtigung der Sommerzeit – führt. Sobald Sie einen rohen Millisekunden-Offset berechnen und ihn anschließend wieder umwandeln, verlassen Sie den Bereich der Temporal-API, der dafür konzipiert ist, dies korrekt zu handhaben.

Was alle fünf Fehler miteinander verbindet

Jedes dieser Probleme hat seine Wurzel in derselben Ursache: Temporal zwingt einen dazu, explizit Zeitzone, Präzision und Absicht anzugeben, und jeder Fehler entsteht dadurch, dass stillschweigend auf die impliziten Annahmen des Date-Typs zurückgegriffen wird. Temporal war niemals als einfacher Ersatz gedacht – es handelt sich um ein strengeres Modell mit Methodennamen, die nur oberflächlich vertraut erscheinen.

Kurzfassung:

  • Rufen Sie niemals Temporal.Now.* auf, ohne explizit zu entscheiden, welche Zeitzone’s „Jetzt“ Sie tatsächlich benötigen
  • Restaurieren Sie Temporal-Werte mit .from() auf beiden Seiten jeder Serialisierungsgrenze – JSON wandelt sie während des Hin- und Rückwegs in einfache Strings um, nicht in lebende Objekte
  • Runden Sie die Genauigkeit nach unten, sodass sie der tatsächlichen Speicherkapazität vor dem Speichern entspricht – und nicht erst später beim Debuggen eines unvorhersehbar fehlgeschlagenen Gleichheitschecks.
  • Für zukünftig datierte Ereignisse speichern Sie die Uhrzeit zusammen mit dem Zeitzone-Informationen und wandeln sie erst in ein Instant um, wenn Sie gerade handeln wollen.
  • Ziehen Sie bei Berechnungen mit Kalendertagen Arithmetik auf Basis von Temporal.Duration – wie .add() und .subtract() – vor, anstatt Berechnungen in Epochen-Millisekunden zu verwenden.
  • Temporal löst tatsächlich viele über ein Jahrzehnt angesammelte Probleme mit Date gut. Allerdings tauscht es dieses alte Chaos gegen eine geringere Anzahl, aber deutlich schärfere Probleme ein. Da sowohl Node 26 als auch Chrome 144 es standardmäßig enthalten, werden immer mehr Teams diese Probleme direkt in der Produktion erleben, während sie umsteigen.