Zeitzonen und Datum in JavaScript: Die häufigsten Fallstricke
AI generated
JS
() =>
JavaScript · Zeitzonen · Debugging
Zeitzonen und Datum in JavaScript
die häufigsten Fallstricke im Überblick

Ein Termin verschiebt sich um eine Stunde, ein Monat wird plötzlich falsch angezeigt, ein Report zeigt in der Nacht der Zeitumstellung ein doppeltes Datum. Zeitzonen und Datum gehören zu den Bereichen von JavaScript, in denen kleine Missverständnisse große, schwer reproduzierbare Bugs erzeugen. Dieser Artikel zeigt die konkreten Fallstricke und wie man sie zuverlässig umgeht.

18 Min. Lesezeit Date · UTC · Sommerzeit · Temporal API Frontend · Backend · verteilte Systeme

1. Warum Datumsverarbeitung in JavaScript so viele Fallstricke hat

Das eingebaute Date-Objekt in JavaScript stammt aus den frühen Tagen der Sprache und wurde nie grundlegend überarbeitet, obwohl seine API zahlreiche Designentscheidungen enthält, die aus heutiger Sicht unglücklich sind. Zeitzonen und Datum wirken auf den ersten Blick simpel, weil jeder Mensch intuitiv mit Uhrzeiten umgeht, doch die Kombination aus lokaler Zeit, UTC, Sommerzeit und regional unterschiedlichen Kalenderregeln erzeugt in der Praxis eine überraschend hohe Zahl an Randfällen.

Der Kern des Problems liegt darin, dass ein Date-Objekt intern immer einen einzigen Zeitpunkt in UTC speichert, jede Ausgabe über Methoden wie getHours() oder toString() aber implizit die Zeitzone der ausführenden Umgebung anwendet. Läuft derselbe Code auf einem Server in einer anderen Zeitzone als im Browser des Nutzers, liefert exakt derselbe Date-Wert unterschiedliche, scheinbar widersprüchliche Ausgaben. Genau diese unsichtbare, implizite Umrechnung ist die Wurzel fast aller Fallstricke rund um Zeitzonen und Datum in JavaScript.

Die folgenden Abschnitte zeigen die konkreten Situationen, in denen Zeitzonen und Datum typischerweise zu Fehlern führen: bei der Verwechslung von UTC und lokaler Zeit, beim Monatsindex, beim Parsen von Datumsstrings, bei der Sommerzeit und bei verteilten Systemen mit Server und Client in unterschiedlichen Zeitzonen.

2. Lokale Zeit vs. UTC: der häufigste Fallstrick

Die Methoden getHours(), getDate() und getMonth() liefern den jeweiligen Wert in der lokalen Zeitzone der ausführenden Umgebung, während die Methoden getUTCHours(), getUTCDate() und getUTCMonth() denselben Wert in UTC zurückgeben. Wer diese beiden Methodenfamilien versehentlich mischt, etwa lokale Stunden mit UTC-Tagen kombiniert, erzeugt Berechnungen, die in den meisten Zeitzonen zufällig funktionieren und exakt in den Randstunden um Mitternacht sichtbar falsch werden.

Ein besonders tückischer Fall entsteht, wenn ein Datum ohne Uhrzeitkomponente als reiner Kalendertag gemeint ist, etwa ein Geburtsdatum oder ein Liefertermin, aber trotzdem als vollständiger Date-Zeitstempel mit Uhrzeit gespeichert wird. Da new Date('2026-07-30') als UTC-Mitternacht interpretiert wird, kann die Anzeige in einer westlich gelegenen Zeitzone den 29. Juli statt des 30. Juli zeigen, weil die lokale Umrechnung den Zeitpunkt in den Vortag verschiebt. Für reine Kalenderdaten ohne Uhrzeitbezug ist es deshalb robuster, ausschließlich mit den UTC-Methoden zu arbeiten oder das Datum als reinen String ohne Zeitkomponente zu behandeln.


// A pure calendar date, no time component intended
const deliveryDate = new Date('2026-07-30'); // parsed as UTC midnight

// In a timezone behind UTC (e.g. US Pacific, UTC-7), this can show the WRONG day
console.log(deliveryDate.getDate());     // 29 — local interpretation shifts it back
console.log(deliveryDate.getUTCDate());  // 30 — correct, matches the intended day

// Safer for calendar-only dates: always read via the UTC methods
function formatCalendarDate(date) {
  const year = date.getUTCFullYear();
  const month = String(date.getUTCMonth() + 1).padStart(2, '0');
  const day = String(date.getUTCDate()).padStart(2, '0');
  return `${year}-${month}-${day}`;
}
console.log(formatCalendarDate(deliveryDate)); // "2026-07-30" — always correct

3. Monate von 0 bis 11: der Off-by-One-Fallstrick

Eine der bekanntesten Designentscheidungen im Date-Objekt ist der nullbasierte Monatsindex: Januar ist Monat 0, Dezember ist Monat 11. Diese Entscheidung orientierte sich ursprünglich an der Java-Date-API der Neunzigerjahre, wurde aber nie korrigiert und sorgt bis heute für einen der häufigsten Fallstricke im Umgang mit Zeitzonen und Datum. Wer new Date(2026, 7, 30) schreibt und dabei den achten Monat, also August, meint, erzeugt tatsächlich ein Datum im September, weil 7 als Index für den achten Monat steht.

Besonders tückisch wird dieser Fallstrick, wenn ein Monatswert aus einem Formularfeld oder einer API im gewohnten, einsbasierten Format ankommt und ungeprüft an den Date-Konstruktor weitergereicht wird. Der resultierende Fehler verschiebt das Datum konsequent um einen Monat, fällt aber oft erst bei Monatsgrenzen oder in Berichten über einen längeren Zeitraum auf, weil die meisten Tests zufällig mit demselben, falschen Offset arbeiten und der Fehler sich dadurch versteckt.


// WRONG: month is 1-based from the form, but Date expects 0-based months
const userMonth = 8; // user means August
const wrongDate = new Date(2026, userMonth, 30); // actually creates September 30

console.log(wrongDate.getMonth()); // 8 — this IS September, not August

// RIGHT: convert explicitly before constructing the Date
const correctDate = new Date(2026, userMonth - 1, 30);
console.log(correctDate.toLocaleDateString('en-US', { month: 'long' })); // "August"

// getMonth() itself also returns 0-based, always add 1 for human-readable output
console.log(`Month: ${correctDate.getMonth() + 1}`); // "Month: 8"

4. Datum als String parsen: Fallstricke bei new Date(string)

Der Date-Konstruktor akzeptiert Strings in unterschiedlichen Formaten, aber nur das ISO-8601-Format ist über alle Umgebungen hinweg standardisiert und zuverlässig. Formate wie 07/30/2026 oder 30.07.2026 werden von Browsern und Node.js teils unterschiedlich interpretiert, teils überhaupt nicht erkannt und liefern dann ein Datum mit dem Wert Invalid Date, ohne dass ein Fehler geworfen wird. Genau dieses stille Scheitern ohne Exception macht Parsing-Fehler bei Zeitzonen und Datum besonders gefährlich, weil nachgelagerte Berechnungen einfach mit NaN weiterlaufen, statt den Fehler sofort sichtbar zu machen.

Ein weiterer Unterschied betrifft die Zeitzone innerhalb des ISO-Strings selbst: 2026-07-30T10:00:00 ohne Zeitzonensuffix wird als lokale Zeit interpretiert, während 2026-07-30T10:00:00Z mit dem Suffix Z als UTC gilt. Fehlt dieses Suffix in Daten, die von einem Backend geliefert werden, das selbst UTC meint, entsteht ein Zeitversatz in Höhe der lokalen Zeitzonendifferenz, der sich in jedem Client mit einer anderen Zeitzone unterschiedlich auswirkt und deshalb im eigenen Test oft unbemerkt bleibt.


// Fragile: format not guaranteed to parse consistently across engines
const fragile = new Date('30.07.2026'); // may be Invalid Date in some engines

// Reliable: ISO 8601 with explicit UTC suffix
const reliable = new Date('2026-07-30T10:00:00Z');
console.log(reliable.toISOString()); // "2026-07-30T10:00:00.000Z"

// Silent failure — always guard against Invalid Date before using the value
function parseStrict(input) {
  const parsed = new Date(input);
  if (Number.isNaN(parsed.getTime())) {
    throw new Error(`Could not parse date: ${input}`);
  }
  return parsed;
}

5. Sommerzeit: wenn Stunden verschwinden oder doppelt existieren

An den beiden Tagen im Jahr, an denen die Sommerzeit beginnt oder endet, existiert lokal entweder eine Stunde weniger oder eine Stunde doppelt. Beim Beginn der Sommerzeit springt die Uhr etwa von 2:00 direkt auf 3:00, die Stunde zwischen 2:00 und 3:00 existiert in dieser Zeitzone an diesem Tag schlicht nicht. Ein Date-Objekt, das für genau diesen nicht existierenden Zeitpunkt konstruiert wird, wird von der Engine automatisch auf einen benachbarten, gültigen Zeitpunkt verschoben, meist eine Stunde später, ohne dass dabei ein Fehler oder eine Warnung ausgegeben wird.

Beim Ende der Sommerzeit existiert die betroffene Stunde dagegen doppelt, wodurch Berechnungen wie die Differenz zwischen zwei Zeitpunkten in Millisekunden systematisch um eine Stunde daneben liegen können, wenn einer der beiden Zeitpunkte in die doppelte Stunde fällt. Für Anwendungen, die exakte Zeitdifferenzen berechnen müssen, etwa Abrechnungssysteme oder Protokollierung, ist es deshalb entscheidend, intern konsequent mit UTC-Zeitstempeln zu rechnen und die lokale Zeitzone ausschließlich für die Anzeige zu verwenden, niemals für Berechnungen.


// Duration calculations must use UTC timestamps, never local wall-clock time
function hoursBetween(start, end) {
  const msPerHour = 1000 * 60 * 60;
  // getTime() always returns milliseconds since epoch in UTC, DST-safe
  return (end.getTime() - start.getTime()) / msPerHour;
}

const beforeDstEnd = new Date('2026-10-25T00:00:00Z');
const afterDstEnd = new Date('2026-10-26T00:00:00Z');
console.log(hoursBetween(beforeDstEnd, afterDstEnd)); // always exactly 24

// WRONG approach: manually adding "1 day" in local wall-clock hours
// can silently be off by one hour around a DST transition

6. Zeitzonen über Server und Client hinweg

In verteilten Systemen läuft der Server meist in UTC, während der Client in der lokalen Zeitzone des Nutzers arbeitet, die sich zwischen zwei Anfragen sogar ändern kann, etwa auf einem Laptop während einer Reise. Wird ein Zeitstempel ohne explizite Zeitzoneninformation zwischen beiden Seiten ausgetauscht, etwa als einfacher String ohne Z-Suffix oder als lokale Uhrzeit ohne Offset, entsteht ein Fallstrick, der sich erst bemerkbar macht, sobald Server und Client tatsächlich unterschiedliche Zeitzonen verwenden, was in lokalen Entwicklungsumgebungen oft gar nicht der Fall ist.

Die robuste Lösung ist, ausschließlich UTC-Zeitstempel im ISO-8601-Format mit explizitem Z-Suffix oder als numerischen Unix-Timestamp über die API auszutauschen, und die Umrechnung in die lokale Zeitzone konsequent erst im letzten Schritt, direkt vor der Anzeige im Client, mit toLocaleString() oder der Intl.DateTimeFormat-API durchzuführen. Diese klare Trennung zwischen Speicherung, Übertragung und Anzeige verhindert, dass Zeitzonen und Datum an mehreren Stellen im System unterschiedlich interpretiert werden.


// Server response: always transmit UTC explicitly
const apiResponse = { createdAt: '2026-07-30T14:30:00Z' };

// Client: convert to the user's local timezone only at display time
const date = new Date(apiResponse.createdAt);
const formatted = new Intl.DateTimeFormat('de-DE', {
  dateStyle: 'medium',
  timeStyle: 'short',
  timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone
}).format(date);

console.log(formatted); // e.g. "30. Juli 2026, 16:30" in Europe/Berlin

7. Datumsarithmetik: Fallstricke beim Addieren von Tagen und Monaten

Das naheliegende Muster, einen Tag zu einem Datum zu addieren, indem 24 Stunden in Millisekunden aufaddiert werden, funktioniert an den meisten Tagen des Jahres korrekt, versagt aber an Sommerzeitübergängen, weil ein Kalendertag an diesen beiden Tagen entweder 23 oder 25 Stunden lang ist. Die robustere Alternative arbeitet direkt mit setDate(getDate() + 1), weil diese Methode kalendarisch denkt und automatisch die korrekte Anzahl an Stunden für den jeweiligen Tag berücksichtigt, unabhängig von einer eventuellen Zeitumstellung.

Beim Addieren von Monaten kommt ein zusätzlicher Fallstrick hinzu: Wird zum 31. Januar ein Monat addiert, existiert der 31. Februar nicht, und die Engine rollt das Datum automatisch in den März weiter, meist auf den 2. oder 3. März statt auf den erwarteten letzten Tag des Februars. Wer Monatsarithmetik für Abrechnungszyklen oder wiederkehrende Termine benötigt, muss diesen Rollover-Fall explizit behandeln, etwa indem nach der Addition geprüft wird, ob der Monatswert wie erwartet um eins gestiegen ist, und andernfalls auf den letzten Tag des Zielmonats korrigiert wird.


// WRONG: adding milliseconds can be off by one hour across a DST transition
function addOneDayUnsafe(date) {
  return new Date(date.getTime() + 24 * 60 * 60 * 1000);
}

// RIGHT: calendar-aware addition, DST-safe
function addOneDaySafe(date) {
  const result = new Date(date);
  result.setDate(result.getDate() + 1);
  return result;
}

// Month rollover trap: January 31 + 1 month should ideally stay in February
const jan31 = new Date(2026, 0, 31);
jan31.setMonth(jan31.getMonth() + 1);
console.log(jan31.toDateString()); // rolls over to March 2 or 3, NOT February

// Explicit fix: clamp to the last day of the target month
function addMonthsClamped(date, months) {
  const result = new Date(date);
  const targetMonth = result.getMonth() + months;
  result.setMonth(targetMonth, 1); // set to day 1 first to avoid rollover
  const lastDay = new Date(result.getFullYear(), result.getMonth() + 1, 0).getDate();
  result.setDate(Math.min(date.getDate(), lastDay));
  return result;
}

8. Die Temporal API als Ausweg: was sich ändert und was nicht

Die Temporal API adressiert genau die hier beschriebenen Fallstricke auf Sprachebene, indem sie separate, unmissverständliche Typen für unterschiedliche Zeitkonzepte einführt: Temporal.PlainDate für einen reinen Kalendertag ohne Uhrzeit und ohne Zeitzone, Temporal.ZonedDateTime für einen Zeitpunkt mit expliziter Zeitzone, und Temporal.Instant für einen zeitzonenunabhängigen Zeitpunkt in UTC. Diese Trennung macht viele der beschriebenen Fallstricke bei Zeitzonen und Datum strukturell unmöglich, weil ein reines Kalenderdatum gar nicht erst mit einer impliziten Zeitzone verwechselt werden kann.

Monate werden in der Temporal API einsbasiert gezählt, der Off-by-One-Fallstrick aus Abschnitt drei entfällt damit vollständig. Arithmetik-Methoden wie add({ months: 1 }) behandeln Sommerzeitübergänge und Monatsrollover explizit und dokumentiert, statt sich implizit aus der Millisekundenrechnung zu ergeben. Zum jetzigen Stand ist die Temporal API in aktuellen Browsern noch nicht überall nativ verfügbar und erfordert teilweise ein Polyfill, langfristig ist sie aber der klar bessere Weg, um Zeitzonen und Datum korrekt zu modellieren, statt die Fallstricke des klassischen Date-Objekts einzeln zu umschiffen.


// Temporal API: explicit types eliminate entire classes of date pitfalls
// (requires a polyfill in engines without native support yet)

// A pure calendar date, no time, no timezone ambiguity possible
const calendarDate = Temporal.PlainDate.from('2026-07-30');
console.log(calendarDate.month); // 7 — one-based, no off-by-one trap

// Explicit timezone, DST-aware arithmetic
const meeting = Temporal.ZonedDateTime.from('2026-07-30T10:00:00[Europe/Berlin]');
const nextWeek = meeting.add({ weeks: 1 }); // handles DST transitions correctly

console.log(nextWeek.toString());

9. Legacy Date vs. Temporal im direkten Vergleich

Die Wahl zwischen dem klassischen Date-Objekt und der neueren Temporal API hängt stark davon ab, welche der beschriebenen Fallstricke im eigenen Projekt tatsächlich relevant sind und wie viel Kontrolle über die Ziel-Laufzeitumgebung besteht.

Aspekt Legacy Date Temporal API Auswirkung
Monatsindex nullbasiert, Off-by-One-Risiko einsbasiert Weniger Verwechslungen bei Monaten
Reines Kalenderdatum immer mit Uhrzeit und Zeitzone vermischt PlainDate ohne Zeitzone Kein UTC-Offset-Fallstrick mehr
Sommerzeit-Arithmetik implizit, fehleranfällig bei Millisekunden explizit dokumentiert und geregelt Vorhersehbares Verhalten an DST-Tagen
Parsing von Strings engine-abhängig, teils Invalid Date strikt nach ISO-8601-Untermenge Konsistentes Verhalten über Engines
Browser-Unterstützung universell, seit Jahrzehnten teils Polyfill nötig Migrationsplanung erforderlich

Für neue Projekte lohnt sich der frühzeitige Blick auf die Temporal API, weil die strukturellen Fallstricke bei Zeitzonen und Datum dort von vornherein vermieden werden, während bestehende Codebasen mit dem klassischen Date-Objekt von den in diesem Artikel gezeigten, gezielten Absicherungen profitieren, ohne eine vollständige Migration durchführen zu müssen.

Mironsoft

JavaScript-Debugging, Code-Reviews und Frontend-Architektur

Zeitzonen-Bugs kosten euch Support-Tickets?

Wir prüfen bestehenden Code auf riskante Zeitzonen- und Datumsverarbeitung, härten die Server-Client-Kommunikation gegen Zeitversatz ab und planen die schrittweise Migration zur Temporal API.

Code-Review

Systematische Suche nach UTC-Verwechslungen und Parsing-Risiken

API-Design

Konsistente UTC-Verträge zwischen Server und Client

Migration

Schrittweise Einführung der Temporal API mit Polyfill-Strategie

10. Zusammenfassung

Zeitzonen und Datum gehören zu den Bereichen von JavaScript, in denen die größte Kluft zwischen scheinbarer Einfachheit und tatsächlicher Komplexität besteht. Der häufigste Fallstrick ist die Verwechslung von lokaler Zeit und UTC, gefolgt vom nullbasierten Monatsindex, inkonsistentem String-Parsing und den beiden Tagen im Jahr, an denen Sommerzeit-Übergänge Stunden verschwinden lassen oder verdoppeln. In verteilten Systemen verschärft sich das Problem, sobald Server und Client tatsächlich in unterschiedlichen Zeitzonen laufen.

Die robuste Grundregel lautet: Intern und bei der Übertragung zwischen Server und Client ausschließlich mit UTC-Zeitstempeln im ISO-8601-Format arbeiten, die Umrechnung in die lokale Zeitzone erst unmittelbar vor der Anzeige durchführen, und für reine Kalenderdaten ohne Uhrzeitbezug konsequent die UTC-Methoden verwenden. Die neue Temporal API löst viele dieser Fallstricke strukturell, indem sie separate Typen für Kalenderdatum, zonenbehaftete Zeit und zeitzonenunabhängigen Zeitpunkt einführt, und ist für neue Projekte einen genaueren Blick wert.

Zeitzonen und Datum in JavaScript, das Wichtigste auf einen Blick

Grundregel

Intern immer UTC, lokale Zeitzone nur direkt vor der Anzeige über Intl.DateTimeFormat anwenden.

Monatsindex

getMonth() ist nullbasiert. Immer explizit plus eins für die menschenlesbare Ausgabe rechnen.

Sommerzeit

Zeitdifferenzen nie mit Millisekunden-Addition berechnen, immer mit getTime()-Differenz oder Temporal.

Zukunftssicher

Temporal API mit PlainDate, ZonedDateTime und Instant vermeidet die strukturellen Fallstricke von Date.

11. FAQ: Zeitzonen und Datum in JavaScript

1Warum zeigt mein Datum den falschen Tag?
Date speichert intern UTC, lokale Methoden wenden aber die Zeitzone der Umgebung an. Für Kalenderdaten die UTC-Methoden nutzen.
2Warum ist Januar der Monat 0?
Historisches Erbe der Java-Date-API. Für Anzeigen immer eins zu getMonth() addieren.
3Warum Invalid Date beim Parsen?
Nur ISO-8601 ist zuverlässig standardisiert. Andere Formate werden je nach Engine unterschiedlich behandelt.
4Was passiert bei einem nicht existierenden Datum?
Die Engine verschiebt es stillschweigend auf den nächsten gültigen Zeitpunkt, meist eine Stunde später.
5Wie berechne ich Zeitdifferenzen sicher?
Immer die getTime()-Differenz in Millisekunden nutzen, niemals lokale Stunden manuell subtrahieren.
6Wie tausche ich Zeitstempel sicher aus?
Immer UTC im ISO-8601-Format mit Z-Suffix übertragen, lokale Umrechnung erst vor der Anzeige.
7Warum rollt mein Monat falsch weiter?
Existiert der Zieltag im Zielmonat nicht, rollt die Engine automatisch weiter. Explizit auf den letzten Tag korrigieren.
8Ist Millisekunden-Addition für einen Tag sicher?
Nein, an Sommerzeit-Umstellungstagen ist ein Kalendertag 23 oder 25 Stunden lang. setDate() ist die sichere Alternative.
9Was macht die Temporal API anders?
Separate Typen für Kalenderdatum, zonenbehaftete Zeit und UTC-Zeitpunkt verhindern strukturelle Verwechslungen.
10Warum funktioniert der Code lokal, aber nicht in Produktion?
Lokale Umgebungen laufen oft in derselben Zeitzone wie der Entwickler-Rechner, Produktionsserver meist in UTC.