Datum und Uhrzeit endlich richtig handhaben
Das Date-Objekt ist einer der ältesten und fehleranfälligsten Teile von JavaScript. Die Temporal API bricht mit allen Designfehlern: immutable Typen, explizite Zeitzonensemantik, korrekte Kalenderarithmetik und eine klare API — ohne externe Bibliotheken wie Moment.js oder date-fns.
Inhaltsverzeichnis
- 1. Warum das Date-Objekt scheitert
- 2. Die Temporal-Typen: PlainDate, Instant, ZonedDateTime und mehr
- 3. PlainDate und PlainTime: Datum ohne Zeitzone
- 4. Instant: der exakte Zeitpunkt in UTC
- 5. ZonedDateTime: Zeitzone explizit und unveränderlich
- 6. Datumsarithmetik: Duration und Differenzen
- 7. Temporal vs. Date: der direkte Vergleich
- 8. Formatierung und Internationalisierung mit Intl
- 9. Migration von Date zu Temporal
- 10. Zusammenfassung
- 11. FAQ
1. Warum das Date-Objekt scheitert
Das JavaScript Date-Objekt wurde 1995 in wenigen Tagen entwickelt und hat seitdem kaum bedeutsame Verbesserungen erfahren. Die Designprobleme sind gut dokumentiert und treffen jeden Entwickler früher oder später: Monate sind 0-basiert (Januar ist 0, Dezember ist 11), getYear() gibt die Zahl der Jahre seit 1900 zurück, und Date-Objekte sind mutierbar — date.setMonth(date.getMonth() + 1) verändert das ursprüngliche Objekt. Das führt zu schwer auffindbaren Bugs, wenn dasselbe Datum-Objekt an mehrere Stellen weitergegeben wird.
Das gravierendste Problem ist jedoch die fehlende Zeitzonenunterstützung: Date kennt nur UTC und die lokale Systemzeitzone des Geräts. Jede andere Zeitzone muss mit manuellen Offset-Berechnungen simuliert werden — ein fehleranfälliger Ansatz, der Sommer- und Winterzeitwechsel ignoriert. Genau das ist der Grund, warum Bibliotheken wie Moment.js, Luxon und date-fns entstanden sind. Die Temporal API macht all das überflüssig: Sie ist das Ergebnis jahrelanger Arbeit der TC39-Arbeitsgruppe und adressiert jeden bekannten Designfehler des Date-Objekts mit einer modernen, unveränderlichen API.
2. Die Temporal-Typen: PlainDate, Instant, ZonedDateTime und mehr
Der wichtigste konzeptionelle Unterschied der Temporal API gegenüber Date ist die Trennung verschiedener Konzepte in eigene Typen. Date versucht, alle Szenarien mit einem einzigen Typ abzudecken — Datum, Uhrzeit, Zeitzone, UTC-Timestamp — und schlägt dabei bei allen. Die Temporal API bietet stattdessen spezialisierte Typen: Temporal.PlainDate für ein Datum ohne Zeit und Zeitzone, Temporal.PlainTime für eine Uhrzeit ohne Datum und Zeitzone, Temporal.PlainDateTime für Datum und Zeit ohne Zeitzone, Temporal.Instant für einen exakten UTC-Zeitpunkt, und Temporal.ZonedDateTime für den vollständigen Typ mit Zeitzone.
Diese Spezialisierung ist keine akademische Übung — sie erzwingt, dass Entwickler explizit angeben, welches Konzept sie meinen. Ein Geburtstag ist ein PlainDate — er hat keine Zeitzone, weil ein Geburtstag in Tokyo derselbe Geburtstag wie in Berlin ist. Ein Serverlog-Eintrag ist ein Instant — er muss in UTC gespeichert werden, unabhängig von der lokalen Zeit des Servers. Eine Kalenderveranstaltung ist ein ZonedDateTime — sie hat eine explizite Zeitzone, weil das Meeting um 10 Uhr in Berlin bei Sommerzeit eine andere UTC-Zeit hat als im Winter. Die Temporal API macht diesen Unterschied zur Pflicht, nicht zur Option.
// Temporal API — distinct types for distinct concepts
import { Temporal } from "@js-temporal/polyfill";
// PlainDate: a date without time or timezone — e.g., a birthday
const birthday = Temporal.PlainDate.from("1990-03-15");
console.log(birthday.year); // 1990
console.log(birthday.month); // 3 — 1-based, unlike Date!
console.log(birthday.day); // 15
// Instant: exact UTC point in time — e.g., log timestamps
const logEntry = Temporal.Now.instant();
console.log(logEntry.epochSeconds); // Unix timestamp
console.log(logEntry.toString()); // "2026-05-10T14:30:00Z"
// ZonedDateTime: date, time AND explicit timezone — e.g., calendar events
const meeting = Temporal.ZonedDateTime.from({
year: 2026, month: 6, day: 15,
hour: 10, minute: 0,
timeZone: "Europe/Berlin"
});
console.log(meeting.toString());
// "2026-06-15T10:00:00+02:00[Europe/Berlin]"
// Convert to another timezone — same instant, different wall clock
const meetingInTokyo = meeting.withTimeZone("Asia/Tokyo");
console.log(meetingInTokyo.hour); // 17 (UTC+9, CEST is UTC+2)
3. PlainDate und PlainTime: Datum ohne Zeitzone
Temporal.PlainDate repräsentiert ein Datum ohne Uhrzeit und ohne Zeitzone. Das klingt simpel, löst aber eine erhebliche Quelle von Bugs: Wenn man mit Date nur das Datum eines Ereignisses speichern will, erzeugt man trotzdem einen UTC-Timestamp mit einer impliziten Uhrzeit (meistens Mitternacht UTC). Das führt dazu, dass dasselbe Datum — je nach Zeitzone des Betrachters — als Vortag angezeigt wird. Temporals PlainDate speichert dagegen explizit nur Jahr, Monat und Tag, ohne jegliche Uhrzeit- oder Zeitzoneninformation.
Ebenso wichtig: In der Temporal API sind alle Objekte immutabel. birthday.add({ years: 1 }) gibt ein neues PlainDate-Objekt zurück — das ursprüngliche Objekt bleibt unverändert. Das eliminiert eine ganze Klasse von Bugs, bei denen Datum-Objekte unbeabsichtigt verändert werden. PlainTime funktioniert analog für Uhrzeiten ohne Datum und Zeitzone. PlainDateTime kombiniert Datum und Uhrzeit, ist aber ebenfalls zeitzonenlos — nützlich für lokale Zeitpläne oder Rezept-Zeitangaben, wo die Zeitzone keine Rolle spielt.
4. Instant: der exakte Zeitpunkt in UTC
Temporal.Instant entspricht konzeptionell dem, was Date eigentlich darstellt: einen exakten Zeitpunkt auf der universellen Zeitachse, ausgedrückt als Nanosekunden seit der Unix-Epoche (1. Januar 1970, 00:00:00 UTC). Der Unterschied: Instant ist immutabel, kennt keine Zeitzone und hat keine Methoden, um lokale Datumseigenschaften abzufragen — das ist absichtlich. Ein Instant hat nur einen Wert: einen exakten Zeitpunkt. Alles andere — Stunde, Tag, Monat — ist eine Interpretation dieses Zeitpunkts in einer bestimmten Zeitzone und damit Sache von ZonedDateTime.
Die Auflösung von Instant ist Nanosekunden, nicht Millisekunden wie bei Date. Das ist für High-Performance-Anwendungen relevant — Temporal.Now.instant() liefert einen nanosekunden-genauen Zeitstempel, sofern die Plattform das unterstützt. Für Datenbankzeitstempel, API-Payloads und Log-Einträge ist Instant der richtige Typ: instant.toString() gibt immer einen ISO-8601-String in UTC aus, unabhängig von der Systemzeitzone des ausführenden Geräts — ein fundamentaler Unterschied zu new Date().toISOString(), das ebenfalls UTC ausgibt, aber auf einem mutierbaren Objekt basiert.
// Instant: precise UTC timestamps with nanosecond resolution
import { Temporal } from "@js-temporal/polyfill";
// Current instant — nanosecond precision
const now = Temporal.Now.instant();
console.log(now.epochNanoseconds); // BigInt: nanoseconds since Unix epoch
console.log(now.epochMilliseconds); // compatible with Date.now()
// Parse from ISO string — always UTC
const ts = Temporal.Instant.from("2026-05-10T14:30:00.123456789Z");
console.log(ts.epochNanoseconds);
// Measuring durations precisely
const start = Temporal.Now.instant();
// ... do work ...
const end = Temporal.Now.instant();
const elapsed = end.since(start);
console.log(`Elapsed: ${elapsed.total("milliseconds")}ms`);
// Convert Instant to ZonedDateTime for human-readable output
const berlin = ts.toZonedDateTimeISO("Europe/Berlin");
console.log(berlin.hour); // 16 (UTC+2 in summer)
console.log(berlin.day); // 10
// Sorting: Instant supports compare()
const timestamps = [ts2, ts1, ts3].sort(Temporal.Instant.compare);
5. ZonedDateTime: Zeitzone explizit und unveränderlich
Temporal.ZonedDateTime ist der mächtigste und vollständigste Typ der Temporal API. Er repräsentiert einen Zeitpunkt mit expliziter Zeitzone und allen Datumseigenschaften. Im Gegensatz zu Instant kennt ZonedDateTime die Zeitzone und berechnet korrekt Sommer- und Winterzeiten, Übergangszeiten und historische Zeitzonenanpassungen. Erstellt man einen ZonedDateTime für den 27. Oktober 2024 um 2:30 Uhr in Europa/Berlin — also genau zur Sommerzeituhr-Umstellung — behandelt die Temporal API diese Mehrdeutigkeit explizit und kann konfiguriert werden, die Sommerzeit- oder Winterzeit-Variante zu wählen.
Für Kalender-Anwendungen, Buchungssysteme und internationale Terminplanung ist ZonedDateTime unverzichtbar. Ein Meeting um 10 Uhr in Berlin bleibt um 10 Uhr, wenn die Sommerzeitumstellung dazwischenliegt — die Temporal API berechnet automatisch die korrekte UTC-Zeit. Das Gegenteil — ein fester UTC-Zeitpunkt — würde das Meeting auf 11 Uhr verschieben, was häufig falsch ist. Die Temporal API macht diesen Unterschied explizit zur Designentscheidung, die der Entwickler treffen muss.
6. Datumsarithmetik: Duration und Differenzen
Datumsarithmetik ist mit Date eine Fehlerquelle: Monate haben unterschiedliche Längen, Schaltjahre, Sommerzeitumstellungen mit 23 oder 25 Stunden. Die Temporal API abstrahiert all das mit dem Temporal.Duration-Typ. Eine Duration kann Jahre, Monate, Wochen, Tage, Stunden, Minuten, Sekunden und Nanosekunden enthalten — und sie werden bei der Arithmetik korrekt auf einen konkreten Datumswert angewendet. date.add({ months: 1 }) auf einem 31-Januar-Datum gibt den 28. Februar zurück — weil der 31. Februar nicht existiert. Das konfigurierbare Overflow-Verhalten erlaubt, stattdessen den letzten Tag des Monats zu wählen.
Differenzen zwischen Datumswerten sind ebenfalls präzise: date1.until(date2, { largestUnit: "months" }) berechnet die Differenz in vollen Monaten und Resttagen — nicht in rohen Millisekunden, die der Entwickler dann manuell in Monate umrechnen müsste. Für Alterungsberechnungen, Vertragslaufzeiten und Zahlungsfristen ist das der korrekte Weg. Die Temporal API unterstützt auch nicht-gregorianische Kalender wie den islamischen, hebräischen und japanischen Kalender — alle mit derselben API, aber kalender-spezifisch korrekter Arithmetik.
// Duration and date arithmetic with Temporal API
import { Temporal } from "@js-temporal/polyfill";
const start = Temporal.PlainDate.from("2026-01-31");
// Adding months: handles end-of-month correctly
const nextMonth = start.add({ months: 1 });
console.log(nextMonth.toString()); // "2026-02-28" — not Feb 31
// Configurable overflow behavior
const constrained = start.add({ months: 1 }, { overflow: "constrain" }); // Feb 28
const rejected = start.add({ months: 1 }, { overflow: "reject" }); // throws
// Calculate age in years, months, days
const birthDate = Temporal.PlainDate.from("1990-03-15");
const today = Temporal.Now.plainDateISO();
const age = birthDate.until(today, { largestUnit: "years" });
console.log(`Age: ${age.years} years, ${age.months} months, ${age.days} days`);
// Contract duration — from signing to expiry
const signed = Temporal.PlainDate.from("2026-02-15");
const expiry = signed.add({ years: 2, months: 3 });
const remaining = Temporal.Now.plainDateISO().until(expiry, { largestUnit: "days" });
console.log(`Contract expires in ${remaining.days} days`);
// DST-aware duration: 2 hours after a time, across DST boundary
const beforeDST = Temporal.ZonedDateTime.from("2026-10-25T01:00:00[Europe/Berlin]");
const afterDST = beforeDST.add({ hours: 2 });
// Correctly accounts for the extra hour during fall-back
7. Temporal vs. Date: der direkte Vergleich
Der Unterschied zwischen Date und der Temporal API ist nicht nur syntaktisch — er ist konzeptionell. Date ist ein einziger, mutierbarer Typ, der alle Zeitkonzepte in einem unzureichenden Interface vereint. Die Temporal API trennt die Konzepte in spezialisierte, immutierbare Typen, die exakt das ausdrücken, was gemeint ist.
| Aspekt | Date (alt) | Temporal API (neu) | Bedeutung |
|---|---|---|---|
| Mutierbarkeit | Mutierbar (setMonth etc.) | Immer immutierbar | Kein unbeabsichtigtes Ändern |
| Monatsindizierung | 0-basiert (Jan = 0) | 1-basiert (Jan = 1) | Keine off-by-one Bugs |
| Zeitzonen | Nur UTC + Systemzone | Jede IANA-Zeitzone | Korrekte DST-Handhabung |
| Auflösung | Millisekunden | Nanosekunden | Präzise Performance-Messung |
| Datumsarithmetik | Manuell in ms rechnen | Duration-Typ, add/until | Schaltjahr/DST automatisch |
Ein besonders kritisches Beispiel: new Date("2026-05-10") parst das Datum als UTC Mitternacht. Zeigt man dieses Datum einem Nutzer in UTC-5 an, erscheint es als 9. Mai — ein Tag zu früh. Die Temporal API löst das mit Temporal.PlainDate.from("2026-05-10") — ein Datum ohne Zeitzone, das immer als 10. Mai dargestellt wird, egal wo der Nutzer ist. Dieser Bug ist in jeder großen Web-Anwendung mindestens einmal aufgetreten und kostet Teams regelmäßig Debugging-Zeit.
8. Formatierung und Internationalisierung mit Intl
Die Temporal API ist eng mit der Intl-API verknüpft, dem eingebauten JavaScript-Internationalisierungsstandard. Statt eigene Formatierungslogik zu implementieren, delegiert Temporal die Ausgabe an Intl.DateTimeFormat. Das bedeutet, dass dieselbe Temporal API-Basis für alle Sprachen und Regionen korrekte Datumsformate liefert: date.toLocaleString("de-DE", { dateStyle: "long" }) für "10. Mai 2026" auf Deutsch, date.toLocaleString("en-US") für "May 10, 2026" auf Englisch.
Besonders leistungsfähig ist die Kombination mit nicht-gregorianischen Kalendern: Temporal.PlainDate.from({ calendar: "islamic", year: 1447, month: 11, day: 1 }) erzeugt ein Datum im islamischen Kalender, das mit denselben Methoden formatiert und für Arithmetik genutzt werden kann. Für internationale Anwendungen, die mehrere Kalender unterstützen müssen, ist die Temporal API der einzige saubere Weg in nativem JavaScript — ohne externe Bibliotheken mit hunderten Kilobyte Gewicht.
9. Migration von Date zu Temporal
Die Temporal API ist noch kein offizieller Standard, befindet sich aber seit Jahren in TC39 Stage 3 und ist für viele aktuelle Browser-Versionen polyfillbar. Der offizielle Polyfill @js-temporal/polyfill implementiert die vollständige API und ist für Node.js und Browser verfügbar. Für neue Projekte empfiehlt sich der sofortige Einsatz des Polyfills; für bestehende Projekte gibt es eine schrittweise Migrationsstrategie.
Der erste Schritt bei der Migration: Alle Stellen identifizieren, an denen new Date() oder Date.now() verwendet wird. Dann entscheiden, welcher Temporal-Typ das Konzept korrekt modelliert — PlainDate für reine Datumsangaben, Instant für Timestamps, ZonedDateTime für lokal interpretierte Zeiten. An der Grenze zur bestehenden Infrastruktur (Datenbank, externe APIs) hilft die Konvertierung: Temporal.Instant.fromEpochMilliseconds(date.getTime()) wandelt ein altes Date-Objekt in einen Instant um. In die andere Richtung: new Date(instant.epochMilliseconds).
// Migration helpers: bridging Date and Temporal API
import { Temporal } from "@js-temporal/polyfill";
// Date -> Temporal: convert legacy timestamps
function fromLegacyDate(date, timeZone = "UTC") {
return Temporal.Instant
.fromEpochMilliseconds(date.getTime())
.toZonedDateTimeISO(timeZone);
}
// Temporal -> Date: interop with APIs expecting Date
function toLegacyDate(temporalValue) {
if (temporalValue instanceof Temporal.Instant) {
return new Date(temporalValue.epochMilliseconds);
}
if (temporalValue instanceof Temporal.ZonedDateTime) {
return new Date(temporalValue.toInstant().epochMilliseconds);
}
throw new TypeError("Expected Instant or ZonedDateTime");
}
// Practical: parse API response date strings safely
function parseApiDate(isoString) {
// API sends "2026-05-10" — store as PlainDate, never as Date
return Temporal.PlainDate.from(isoString);
}
// Practical: format for display in user's local timezone
function formatForUser(instant, locale, timeZone) {
const zdt = instant.toZonedDateTimeISO(timeZone);
return zdt.toLocaleString(locale, {
dateStyle: "long",
timeStyle: "short"
});
}
const ts = Temporal.Now.instant();
console.log(formatForUser(ts, "de-DE", "Europe/Berlin"));
// e.g., "10. Mai 2026, 16:30 Uhr"
10. Zusammenfassung
Die Temporal API ist der lang erwartete Ersatz für das fehlerhafte JavaScript Date-Objekt. Mit spezialisierten, immutablen Typen — PlainDate, PlainTime, PlainDateTime, Instant und ZonedDateTime — löst sie alle bekannten Probleme: 0-basierte Monate, Mutierbarkeit, fehlende Zeitzonenunterstützung und ungenaue Datumsarithmetik. Jeder Typ modelliert exakt das Konzept, das der Entwickler meint — kein implizites UTC-Mitternacht mehr bei reinen Datumsangaben, keine fehlenden DST-Korrekturen bei Zeitzonenoperationen.
Mit dem offiziellen Polyfill @js-temporal/polyfill ist die Temporal API heute in jedem Projekt einsetzbar. Die Migration bestehender Date-Nutzung ist schrittweise möglich: Neue Teile der Anwendung nutzen sofort Temporal; Konvertierungsfunktionen überbrücken die Grenze zu alten Teilen und externer Infrastruktur. Wer heute anfängt, Temporal API zu nutzen, schreibt Code, der bei der nativen Unterstützung in allen Browsern ohne Polyfill weiterläuft — die API selbst bleibt stabil, nur der Polyfill-Import entfällt.
Mironsoft
JavaScript-Modernisierung, API-Migration und internationale Web-Anwendungen
Datum-Bugs im Produktivsystem? Wir helfen.
Zeitzonenfehler, Off-by-one-Bugs bei Monaten und DST-Probleme in bestehenden Anwendungen — wir migrieren Date-basierten Code zur Temporal API und beseitigen die Ursache, nicht nur die Symptome.
Fehleranalyse
Zeitzonenfehler, DST-Bugs und Datumsarithmetik-Probleme in bestehenden Anwendungen identifizieren
Temporal-Migration
Schrittweise Migration von Date-Objekten zur Temporal API mit vollständiger Regressionstests
Internationalisierung
Mehrsprachige Datumsformatierung und Kalenderunterstützung mit Temporal + Intl
JavaScript Temporal API — Das Wichtigste auf einen Blick
Typen-Übersicht
PlainDate/Time: ohne Zeitzone. Instant: exakter UTC-Punkt. ZonedDateTime: vollständig mit IANA-Zeitzone. Duration: für Arithmetik. Alle immutierbar.
Kritische Verbesserungen
1-basierte Monate. Kein UTC-Mitternacht-Bug bei PlainDate. DST-korrekte Arithmetik. Nanosekunden-Auflösung. Nicht-gregorianische Kalender nativ.
Einstieg heute
npm install @js-temporal/polyfill — vollständige API, stabile Implementierung. Bei nativer Browser-Unterstützung nur den Import entfernen.
Migration
Instant.fromEpochMilliseconds(date.getTime()) für den Übergang. new Date(instant.epochMilliseconds) zurück. Schrittweise pro Modul migrieren.