Zeitzonen in JavaScript handhaben, ohne den Verstand zu verlieren
JavaScript-Zeitzonen sauber handhaben mit UTC-Instants, IANA-Zonen, Intl.DateTimeFormat, Temporal und DST-sicheren Regeln für Speichern und Anzeigen.
Speichern und übertragen Sie jeden Zeitstempel als UTC-Instant im ISO-8601-Format (z. B. 2026-05-22T08:00:00Z), halten Sie den IANA-Zeitzonenbezeichner (z. B. America/New_York) in einem separaten Feld vor, und konvertieren Sie in die Ortszeit ausschließlich zum Zeitpunkt der Anzeige — speichern Sie niemals eine Wanduhrzeit ohne ihre Zeitzone. Diese eine Regel verhindert die meisten JavaScript-Zeitzonenfehler, und sie gilt unabhängig davon, ob Sie Date, Intl, eine Bibliothek oder die neue Temporal-API verwenden.
Dieser Leitfaden erläutert, warum das eingebaute Date-Objekt Zeitzonen so problematisch macht, welche dauerhaften Regeln das Problem unabhängig vom eingesetzten Werkzeug lösen, wie man Datumsangaben heute mit Intl.DateTimeFormat korrekt formatiert, was Temporal verändert, nachdem es in ES2026 aufgenommen wurde, welche Bibliothek man im Produktionsbetrieb ab Juni 2026 einsetzen sollte, und welche Sonderfälle rund um die Sommerzeit die schwer reproduzierbaren Fehler verursachen.
Wichtigste Erkenntnisse
- Speichern und übertragen Sie Instants in UTC (ISO 8601 oder Epoch), halten Sie den IANA-Zonenbezeichner in einem separaten Feld vor, und konvertieren Sie in die Ortszeit ausschließlich bei der Anzeige.
- Javascripts
Datebietet keine Unterstützung für benannte Zeitzonen — es kann einen Moment nur in UTC oder in der Zone des Host-Systems repräsentieren, was die eigentliche Ursache des Problems „richtiges Datum auf meinem Rechner, falsches Datum für den Nutzer” ist. - Ein zukünftiges Ereignis muss mit seiner IANA-Zone gespeichert werden, nicht als fester UTC-Instant, damit es auch dann zur richtigen Wanduhrzeit aufgelöst wird, wenn sich die Sommerzeitregeln der betreffenden Region vor dem Ereignisdatum ändern.
- Stand Juni 2026 ist
Temporalein Stage-4-Proposal in ECMAScript 2026, das nativ in Firefox 139+, Chromium 144+ und Node.js 26+ ausgeliefert wird, jedoch nicht in Safari — Produktionscode benötigt daher in der Regel noch@js-temporal/polyfillodertemporal-polyfill. - Wenn Sie
Temporalnoch nicht einsetzen können, verwenden Sie Luxon 3.7.2 oder date-fns 4.4.0 mit@date-fns/tz— und beachten Sie, dass das ältere Paketdate-fns-tzauf date-fns v3 ausgerichtet ist, nicht auf v4.
Warum Javascripts Date einen in den Wahnsinn treibt
Das Date-Objekt weist drei strukturelle Mängel auf, wobei der dritte der eigentliche Zeitzonen-Killer ist. Erstens ist es veränderlich: Methoden wie setMonth und setFullYear modifizieren das ursprüngliche Objekt direkt, sodass die Übergabe eines Date-Objekts an eine Funktion es für alle anderen Aufrufer stillschweigend verändern kann. Zweitens ist seine Nummerierung inkonsistent — Monate sind nullbasiert (Januar ist 0, Dezember ist 11), während Monatstage einbasiert sind — was zu Einzel-Monat-Verschiebungsfehlern führt, die Code-Reviews überstehen.
Drittens, und am folgenreichsten: Date bietet keine echte Zeitzonenunterstützung. Es kann einen Moment nur in UTC oder in der lokalen Zone des Host-Systems repräsentieren, und sonst nichts — es gibt keine Möglichkeit, ein Date-Objekt „in America/New_York” zu erstellen oder damit zu arbeiten, wie man es erwarten würde. Der offizielle TC39-Proposal-Text benennt dies klar: Das altehrwürdige ECMAScript-Date-Objekt weist eine Reihe von Herausforderungen auf, darunter fehlende Unveränderlichkeit, fehlende Zeitzonenunterstützung, fehlende Unterstützung für Anwendungsfälle, die nur Datumsangaben oder nur Zeitangaben erfordern, sowie eine verwirrende und wenig ergonomische API.
Die Darstellung anhand der Host-Zone ist der Grund, warum derselbe Code für einen Entwickler in Berlin das richtige Datum anzeigt, für einen Nutzer in Los Angeles jedoch das falsche. Betrachten Sie diese 8-zeilige Reproduktion, die Sie mit Node ausführen können:
// repro.js — ausführen mit: TZ=America/Los_Angeles node repro.js
// und erneut mit: TZ=Europe/Berlin node repro.js
const instant = new Date("2026-03-15T23:30:00Z"); // ein fester UTC-Moment
console.log(instant.toLocaleDateString());
// TZ=America/Los_Angeles → "3/15/2026" (16:30 Ortszeit, noch der 15.)
// TZ=Europe/Berlin → "3/16/2026" (00:30 Ortszeit, bereits der 16.)
Ein Instant, zwei verschiedene Kalenderdaten, die allein von der Zone des Host-Systems abhängen. Der Fehler ist für denjenigen unsichtbar, der ihn geschrieben hat, weil sein Rechner in einer einzigen Zone sitzt. Dieser Host-Zonen-Datumsfehler ist der klassische „funktioniert auf meinem Rechner”-Defekt.
Die dauerhaften Regeln, die Zeitzonen lösen (unabhängig von jeder Bibliothek)
Discover how at OpenReplay.com.
Die folgenden Regeln verhindern Zeitzonenfehler, unabhängig davon, welche API oder Bibliothek Sie verwenden. Sie sind die eigentliche Lösung; die nachfolgend vorgestellten Werkzeuge sind lediglich verschiedene Wege, sie anzuwenden.
- Speichern und übertragen Sie Instants in UTC. Persistieren Sie Zeitstempel als ISO 8601 mit dem Suffix
Z(2026-05-22T08:00:00Z) oder als Epoch-Wert. UTC ist eindeutig und verschiebt sich niemals. - Halten Sie den IANA-Zonenbezeichner in einem separaten Feld vor. Eine Zone wie
Europe/Londonenthält die Sommerzeitregeln, die ein bloßer Offset nicht transportieren kann. Speichern Sie den Bezeichner, nicht einen rohen Offset wie+01:00. - Konvertieren Sie in die Ortszeit nur am Rand — zum Zeitpunkt der Anzeige. Halten Sie alles in UTC durch Ihre Speicherung, Übertragung und Geschäftslogik; lokalisieren Sie ausschließlich in der Darstellungsschicht.
- Unterscheiden Sie zwischen einem absoluten Instant und einer Wanduhrzeit in einer Zone. Ein Protokolleintrag oder ein „erstellt am”-Wert ist ein Instant. Eine Besprechung im Kalender einer Person ist eine Wanduhrzeit, die an eine Zone gebunden ist. Es handelt sich um verschiedene Datentypen, die unterschiedlich modelliert werden müssen.
- Speichern Sie zukünftige Ereignisse als zonenbehaftet, nicht als festen UTC-Instant. Dies ist die Regel, die fast niemand formuliert. Wenn ein Nutzer eine 9:00-Uhr-Besprechung in
America/New_Yorkzwei Jahre im Voraus plant und die Region später ihre Sommerzeitregeln ändert, löst ein heute eingefrorener UTC-Zeitstempel zur falschen Wanduhrzeit auf. Die Speicherung der Zone ermöglicht es, den Instant neu zu berechnen, wenn das Datum eintrifft.
Diese letzte Regel hat eine primärquellenbasierte Grundlage. Der Serialisierungsstandard, den Temporal verwendet, RFC 9557 (das Internet Extended Date/Time Format, veröffentlicht im April 2024), existiert genau deshalb, weil, wie Igalia anmerkt, Temporal einen Standardweg zur Serialisierung von Zeitstempeln mit Zeitzonen- und Kalenderinformationen benötigt, die weit verbreiteten Konventionen — wie das Anhängen von IANA-Zeitzonennamen an Zeitstempel — jedoch nie auf einem formalen Standardisierungspfad lagen. MDN liefert die operative Version der Regel für Offset- versus benannte Zonen: Vermeiden Sie die Verwendung von Offset-Bezeichnern, wenn ein benannter Zeitzonenbezeichner verfügbar ist. Selbst wenn eine Region stets einen einzigen Offset verwendet hat, ist es besser, den benannten Bezeichner zu verwenden, um gegen zukünftige politische Änderungen des Offsets gewappnet zu sein.
Lokalisierte Anzeige heute mit Intl.DateTimeFormat
Für eine korrekte lokalisierte Anzeige verwenden Sie jetzt Intl.DateTimeFormat mit einer expliziten timeZone-Option. Es ist das einzige eingebaute Objekt, das benannte Zonen korrekt verarbeitet, es ist in jedem modernen Browser und in Node verfügbar, und es arbeitet nahtlos sowohl mit Date als auch mit Temporal zusammen.
const instant = new Date("2026-03-15T23:30:00Z");
new Intl.DateTimeFormat("de-DE", {
timeZone: "America/New_York",
dateStyle: "full",
timeStyle: "short",
}).format(instant);
// "Sonntag, 15. März 2026 um 19:30"
Die explizite Übergabe von timeZone macht diesen Ansatz sicher: Sie sind nicht länger der Zone des Host-Systems ausgeliefert. Die Darstellung desselben Instants für einen anderen Nutzer erfordert lediglich die Änderung einer Zeichenkette. Dies ist die „am Rand konvertieren”-Regel in Code — halten Sie den UTC-Instant überall vor, und überlassen Sie Intl die Lokalisierung in der Darstellungsschicht.
Temporal: Die in die Sprache integrierte Lösung für Zeitzonenfehler
Temporal ist der lang versprochene Ersatz für Date, und ab 2026 ist er Realität. Nach 9 Jahren Entwicklungsarbeit erreichte Temporal auf dem TC39-Meeting im März 2026 offiziell Stage 4 und wurde damit Teil von ECMAScript 2026. Das Proposal-Repository bestätigt den Status direkt: Dieses Proposal befindet sich derzeit in Stage 4. Es wird in die Standards ECMA-262 und ECMA-402 eingearbeitet, und dieses Repository wird archiviert.
Temporal ersetzt Date durch einen Namensraum unveränderlicher, zweckspezifischer Typen. Die drei, die Sie am häufigsten verwenden werden:
Temporal.Instant— ein exakter Zeitpunkt (ein Nanosekunden-Zeitstempel), ohne Kalender oder Zone. Verwenden Sie ihn für die UTC-Instants aus Regel 1.Temporal.ZonedDateTime— ein Instant plus eine IANA-Zeitzone plus ein Kalender. MDN beschreibt ihn als Brücke zwischen einer exakten Zeit und einer Wanduhrzeit: Er repräsentiert gleichzeitig einen Moment in der Geschichte und eine lokale Wanduhrzeit. Es ist die einzige Temporal-Klasse, die zeitzonenbewusst ist. Verwenden Sie ihn für zonenbehaftete zukünftige Ereignisse (Regel 5).Temporal.PlainDate/Temporal.PlainTime— ein Kalenderdatum oder eine Uhrzeit ohne jegliche Zone, für Dinge wie Geburtstage und Ladenöffnungszeiten.
Arithmetische Operationen sind unveränderlich — jede Operation gibt einen neuen Wert zurück — und die Konvertierung eines Moments zwischen Zonen ist explizit:
const callAmsterdam = Temporal.ZonedDateTime.from(
"2026-04-24T15:00:00[Europe/Amsterdam]"
);
const callNewYork = callAmsterdam.withTimeZone("America/New_York");
callNewYork.toString();
// "2026-04-24T09:00:00-04:00[America/New_York]"
Temporal schließt auch eine gefährliche Date-Falle: Vergleichsoperatoren auf Temporal-Objekten werfen absichtlich einen TypeError, da ohne valueOf() Ausdrücke mit arithmetischen Operatoren wie plainDate1 > plainDate2 auf plainDate1.toString() > plainDate2.toString() zurückfallen würden. Verwenden Sie stattdessen Temporal.compare() oder .equals() — Temporal.compare() ordnet zwei zonenbehaftete Werte nach ihrem zugrundeliegenden Instant, behandelt also 9:30 Uhr New York und 14:30 Uhr London als gleich, während .equals() sie als verschieden meldet, da es auch Zeitzone und Kalender vergleicht. Die vollständige Typliste finden Sie in der MDN-Temporal-Referenz.
Temporal-Browser- und Laufzeitunterstützung (Stand Juni 2026)
Temporal wird ausgeliefert, aber noch nicht überall. Native Unterstützung wurde in Firefox 139 eingeführt, der als erster Browser Temporal standardmäßig auslieferte — im Mai 2025, gefolgt von Chrome 144 im Januar 2026. Edge basiert auf derselben Chromium-Engine, und auch Node.js hat es ausgeliefert: Node.js 26, veröffentlicht am 5. Mai 2026 mit V8 14.6 und Undici 8, aktivierte Temporal ohne Flags oder experimentelle Einstellungen. Zum ersten Mal in der Geschichte von JavaScript verfügen Entwickler über eine erstklassige Datums-/Zeit-API, die direkt in die Laufzeitumgebung integriert ist.
Die Lücke ist Safari, das es noch nicht ausgeliefert hat — und genau deshalb markiert MDN Temporal als noch nicht Baseline. Für browserübergreifenden Produktionscode benötigen Sie weiterhin einen Polyfill. Es gibt zwei: @js-temporal/polyfill, gepflegt von den Proposal-Champions, und temporal-polyfill, eine kleinere, schnellere Alternative des FullCalendar-Teams, die browserübergreifende Kompatibilität für die verbleibenden Browser bietet. Ihr offizieller Status im Proposal-Repository ist Alpha/Beta statt einer stabilen Version 1.0 — pinnen Sie daher eine Version und testen Sie vor dem Einsatz. Überprüfen Sie die komprimierte Bundle-Größe auf Bundlephobia, bevor Sie sie einplanen; veröffentlichte Angaben variieren stark.
Welche Bibliothek im Produktionsbetrieb jetzt einsetzen
Wenn Sie sich nicht auf natives Temporal für alle Ihre Zielumgebungen verlassen können — und die meisten Produktionsanwendungen können das nicht, bis Safari es ausliefert und Sie den Polyfill entfernt haben — greifen Sie auf eine der folgenden Optionen zurück. Das Fazit, Stand Juni 2026:
| Werkzeug | Zeitzonenbewusst? | Unveränderlich? | Heute nativ? | Verwenden wenn |
|---|---|---|---|---|
Date + Intl.DateTimeFormat | Nur Anzeige | Nein (Date ist veränderlich) | Ja | Minimale Anforderungen; Formatierung eines vorhandenen Instants |
| Luxon 3.7.2 | Ja (IANA) | Ja | Ja | Neuer Code, der eine ergonomische, unveränderliche API nahe an Temporal wünscht |
date-fns 4.4.0 + @date-fns/tz | Ja (IANA) | Ja | Ja | Tree-shakeable-Codebasen mit einem Import pro Funktion |
| Day.js + utc/timezone-Plugins | Ja (IANA) | Ja | Ja | Kleinster Footprint; Migration von Moment.js |
Temporal (nativ oder Polyfill) | Ja (erstklassig) | Ja | Teilweise | Kontrollierte Evergreen-/Node-Umgebungen oder mit Polyfill |
Die wichtigste Genauigkeitsfalle liegt in der date-fns-Spalte. Die Zeitzonenunterstützung hat sich zwischen den Hauptversionen geändert: Ab v4 bietet date-fns erstklassige Zeitzonenunterstützung. Sie wird über die Pakete @date-fns/tz und @date-fns/utc bereitgestellt. Der v4-Ansatz ist die TZDate-Klasse und der tz()-Helfer aus @date-fns/tz (v1.5.0). Das ältere Paket date-fns-tz (v3.2.0) richtet sich an date-fns v3 und ist darüber explizit — seine eigene Dokumentation besagt, es zu verwenden, wenn Sie Zeitzonenunterstützung vor date-fns v4 benötigen. Mischen Sie die beiden nicht.
Die Sommerzeitfälle, die wirklich zuschlagen
Die Sommerzeit erzeugt zwei Fehlermodi, und beide werden in den meisten Codebasen zu wenig getestet. Im Herbst („Uhren zurückstellen”) tritt eine lokale Stunde zweimal auf, sodass eine Wanduhrzeit wie 01:05 mehrdeutig ist. Im Frühling („Uhren vorstellen”) existiert eine lokale Stunde überhaupt nicht, sodass eine Zeit wie 02:05 ungültig ist.
Temporal löst beide deterministisch auf. Es wird mit dem Disambiguierungsverhalten „compatible” aufgelöst: Bei übersprungenen Zeitübergängen wird der spätere der beiden möglichen Instants verwendet, und bei wiederholten Zeitübergängen wird der frühere der beiden möglichen Instants verwendet. Die Ausgaben:
// Uhren zurückstellen: 01:05 tritt in New York am 2024-11-03 zweimal auf
Temporal.ZonedDateTime.from("2024-11-03T01:05:00[America/New_York]").toString();
// "2024-11-03T01:05:00-04:00[America/New_York]" (Standard: der frühere Instant)
Temporal.ZonedDateTime.from("2024-11-03T01:05:00[America/New_York]",
{ disambiguation: "later" }).toString();
// "2024-11-03T01:05:00-05:00[America/New_York]" (das zweite Auftreten)
// Uhren vorstellen: 02:05 existiert in New York am 2024-03-10 nicht
Temporal.ZonedDateTime.from("2024-03-10T02:05:00[America/New_York]").toString();
// "2024-03-10T03:05:00-04:00[America/New_York]" (Standard: springt eine Stunde vor)
Für die übersprungene Stunde können Sie auch disambiguation: "reject" übergeben, um statt einer stillen Auflösung eine Ausnahme zu werfen — nützlich, wenn eine Buchung auf eine nicht existierende Zeit fällt und Sie lieber den Nutzer auffordern möchten, als zu raten. Mit Date wird nichts davon für Sie gehandhabt, und der Fehler tritt nur für Nutzer in Zonen auf, die die Sommerzeit beachten, und zwar an den zwei Tagen im Jahr, an denen der Übergang stattfindet.
Zeitzonendefekte sind schwer zu beheben, eben weil sie sich in der eigenen Zone des Entwicklers nicht reproduzieren lassen. Ein Countdown zeigt negative Werte an, eine Ereigniskarte zeigt den falschen Tag, eine Buchung landet auf der falschen Seite einer Sommerzeitgrenze — aber nur für den Nutzer, niemals auf dem Rechner, der den Code geschrieben hat. Session-Replay ist oft der einzige praktikable Weg, diese Lücke zu schließen: Die Wiedergabe einer in der Umgebung des Nutzers aufgezeichneten Sitzung ermöglicht es einem Entwickler in Europe/Berlin, das genaue falsche Datum zu sehen, das ein Nutzer in America/Los_Angeles gesehen hat, anstatt es sich vorstellen zu müssen.
Wie es weitergeht
Das Heilmittel gegen Zeitzonenfehler ist keine Bibliothek — es ist die Disziplin, Instants in UTC zu speichern, die IANA-Zone beizubehalten, nur bei der Anzeige zu konvertieren und zukünftige Ereignisse als zonenbehaftet zu modellieren. Wenden Sie diese Regeln zuerst an, und wählen Sie dann das Werkzeug: natives Temporal, wo Ihre Umgebungen es unterstützen, einen Polyfill, wo sie es nicht tun, und Luxon oder date-fns v4 mit @date-fns/tz für alles dazwischen. Beginnen Sie damit, eine Stelle in Ihrer Codebasis zu prüfen, an der eine Wanduhrzeit ohne ihre Zone gespeichert wird — dieses Feld ist mit ziemlicher Sicherheit der Ort, an dem Ihr nächster Tages-Verschiebungsfehler wartet.
Häufig gestellte Fragen
Warum zeigt mein Datum für manche Nutzer einen Tag daneben, aber nicht für mich?
Ein JavaScript-Date speichert nur einen UTC-Instant, und Methoden wie toLocaleDateString rendern ihn in der Zone des Host-Systems. Ein fester Instant wie 2026-03-15T23:30:00Z wird unter America/Los_Angeles als 15. März (16:30 Ortszeit), aber unter Europe/Berlin als 16. März (00:30 Ortszeit) dargestellt. Der Code ist korrekt; das Kalenderdatum unterscheidet sich, weil die Darstellungszone unterschiedlich ist. Deshalb lässt sich der Fehler in der eigenen Zeitzone des Entwicklers nie reproduzieren.
Sollte ich eine zukünftige Besprechungszeit als UTC-Zeitstempel speichern?
Nein. Speichern Sie ein zukünftiges Ereignis als Wanduhrzeit, die an ihre IANA-Zone gebunden ist, etwa 2026-05-22T09:00:00 mit America/New_York als Begleitinformation, nicht als eingefrorenen UTC-Instant. Wenn die Region ihre Sommerzeitregeln zwischen jetzt und dem Ereignisdatum ändert, löst ein heute berechneter UTC-Zeitstempel zur falschen Wanduhrzeit auf, während der zonenbehaftete Wert neu berechnet werden kann. UTC-Instants sind korrekt für Protokolle und vergangene Ereignisse, nicht für zukünftige Termine.
Was ist der Unterschied zwischen date-fns-tz und @date-fns/tz?
Sie richten sich an verschiedene Hauptversionen und sind nicht austauschbar. Das ältere Paket date-fns-tz (v3.2.0) bietet Zeitzonenunterstützung ausschließlich für date-fns v3. Ab date-fns v4 wurde die Zeitzonenverarbeitung in das separate Paket @date-fns/tz (v1.5.0) verschoben, das die Klasse TZDate und den Helfer tz bereitstellt. Wenn Sie date-fns 4.x verwenden, nutzen Sie @date-fns/tz; das Mischen der beiden gegen die falsche Hauptversion ist eine häufige Quelle falscher Konvertierungen.
Kann ich die Temporal-API im Produktionsbetrieb 2026 verwenden?
Teilweise. Stand Juni 2026 ist Temporal ein Stage-4-Proposal in ECMAScript 2026 und wird nativ in Firefox 139+, Chromium 144+ (Chrome und Edge) sowie Node.js 26+ ausgeliefert. Safari hat es noch nicht implementiert, weshalb MDN Temporal als noch nicht Baseline markiert. In kontrollierten Evergreen- oder Serverumgebungen können Sie es nativ verwenden; für breite Browserunterstützung benötigen Sie weiterhin @js-temporal/polyfill oder temporal-polyfill, die beide Alpha- oder Beta-Status haben — pinnen Sie daher eine Version und testen Sie zuerst.
Complete picture for complete understanding
Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.
Star on GitHub12k