mitmario.dev

JavaScript Date: Fünf Fallen und wie Temporal sie löst

10 Min Lesezeit Mario Laurich

Der Geburtstag, der im Kalender plötzlich einen Tag früher steht. Der Monat, der „7“ heißt und August meint. Die Frist, die eine Stunde verrutscht, weil irgendwo im März jemand an der Uhr gedreht hat. Wenn dir eins davon bekannt vorkommt: Willkommen bei Date. Ich bin seit Jahren Stammgast und falle trotzdem noch rein.

Aufgeschrieben habe ich das jetzt, weil Node.js 26 laut offiziellem Releaseplan am 28. Oktober 2026 zur LTS-Version wird. Im Release-Post zu Node 26 steht der Satz „The Temporal API is now enabled by default in Node.js 26“. Temporal ist der Nachfolger von Date, und er ist dort ohne Flag einfach da.

Hier sind die fünf Fallen, die mich am häufigsten erwischt haben, jeweils mit dem Date-Code, dem, was rauskommt, und derselben Stelle mit Temporal.

Jedes Beispiel habe ich in einem node:26-Container ausgeführt, mit gesetzter Zeitzone (TZ=Europe/Berlin, bei der zweiten Falle zusätzlich TZ=America/New_York). Die Ausgaben stehen als Kommentare im Code, so wie sie rauskamen. Bei dir kann die Zeitzone eine andere sein, und an einigen Stellen geht es um nichts anderes.

Ein leerer Wandkalender an einer hellen Wand, eine Wochenzeile ist um einen Tag nach rechts verrutscht und leuchtet orange
Titelbild mit KI erstellt. Der Text entsteht mit KI-Hilfe, geprüft und überarbeitet von mir. Mehr dazu

Die fünf Fallen von Date

Falle 1: Der Monat zählt ab null

Der Klassiker, und auch mir passiert er hin und wieder noch. Der Tag zählt ab eins, der Monat ab null:

const d = new Date(2026, 7, 3);

console.log(d.getMonth());      // 7
console.log(d.toDateString());  // Mon Aug 03 2026

Wer „3. Juli“ meint und 7 schreibt, bekommt den August und keine Fehlermeldung. Es ist ja auch ein gültiges Datum, Date hat keinen Grund, sich zu beschweren. In der Lektion Datum und Zeit im JavaScript-Kurs steht das genauso drin. Anfänger erwischt es zuverlässig, Fortgeschrittene auch.

Mit Temporal schreibst du den Monat so, wie du ihn sprichst:

const t = Temporal.PlainDate.from({ year: 2026, month: 8, day: 3 });

console.log(t.month);      // 8
console.log(t.toString()); // 2026-08-03

Eine Einschränkung, damit ich dir hier nichts verkaufe: Auch Temporal ist nicht streng, solange du es nicht verlangst. Ein Monat 13 ist bei Date ein Sprung ins nächste Jahr, bei Temporal wird er standardmäßig auf den Dezember zurechtgestutzt:

console.log(new Date(2026, 12, 3).toDateString());
// Sun Jan 03 2027

console.log(Temporal.PlainDate.from({ year: 2026, month: 13, day: 3 }).toString());
// 2026-12-03

Temporal.PlainDate.from({ year: 2026, month: 13, day: 3 }, { overflow: "reject" });
// RangeError

Der Schalter heißt overflow. Standard ist "constrain" (zurechtstutzen), mit "reject" wirft Temporal einen RangeError. Wenn Eingaben von Nutzern kommen, nehme ich "reject".

Falle 2: Derselbe Text, zwei verschiedene Zeiten

Diese Falle ist besonders gemein, weil sie lange gut geht. Zwei Schreibweisen, die fast gleich aussehen:

const a = new Date("2026-08-03");
const b = new Date("2026-08-03T00:00");

Ein Datumstext ohne Uhrzeit liest JavaScript als UTC, einer mit Uhrzeit (aber ohne Zeitzone) als lokale Zeit. Das Ergebnis hängt also davon ab, wo der Code läuft:

Zeitzonea.toISOString()b.toISOString()a.getDate()b.getDate()
Europe/Berlin2026-08-03T00:00:00.000Z2026-08-02T22:00:00.000Z33
America/New_York2026-08-03T00:00:00.000Z2026-08-03T04:00:00.000Z23
Ausgabe in node:26, je mit gesetzter TZ

In Berlin merkst du nichts, beide Varianten zeigen den 3. In New York wird aus a der 2. August, weil 00:00 UTC dort noch der Abend des Vortags ist. Auch a.toLocaleDateString("de-DE") gibt dort 2.8.2026 aus. Das ist der Geburtstag, der einen Tag früher steht. Steht dein Rechner in Berlin, fällt es erst auf, wenn jemand aus einer anderen Zeitzone dazukommt.

Mit Temporal fehlt die Zeitzone, wo sie nichts zu suchen hat. Ein PlainDate ist ein Kalendertag und sonst nichts:

const p = Temporal.PlainDate.from("2026-08-03");

console.log(p.toString()); // 2026-08-03
console.log(p.day);        // 3

Das kam in Berlin und in New York identisch raus. Verschieben kann sich da nichts. Ohne Uhrzeit gibt es nichts, was man in eine andere Zone umrechnen müsste.

Falle 3: Date lässt sich verändern

setDate, setMonth und die anderen Setter ändern das Objekt, auf dem du sie aufrufst. Das ist harmlos, bis dasselbe Objekt woanders auch benutzt wird:

function frist(d) {
  d.setDate(d.getDate() + 14);
  return d;
}

const bestellt = new Date(2026, 7, 3);
const lieferung = frist(bestellt);

console.log(bestellt.toDateString());   // Mon Aug 17 2026
console.log(lieferung.toDateString());  // Mon Aug 17 2026
console.log(bestellt === lieferung);    // true

Die Bestellung ist jetzt auf den 17. August datiert. Die Funktion hat ihr Argument nicht kopiert, sie hat es verändert, und return d gibt dasselbe Objekt zurück.

Temporal-Objekte sind unveränderlich. Jede Rechenoperation gibt ein neues zurück:

const bestellt = Temporal.PlainDate.from("2026-08-03");
const lieferung = bestellt.add({ days: 14 });

console.log(bestellt.toString());  // 2026-08-03
console.log(lieferung.toString()); // 2026-08-17

Bei Date hilft nur, vorher selbst eine Kopie zu ziehen (new Date(d)). Bei Temporal muss man daran nicht denken.

Falle 4: Ein Tag hat nicht immer 24 Stunden

Wie viele Tage liegen zwischen dem 28. und dem 30. März 2026? Zwei. Der Klassiker rechnet über Millisekunden:

const MS_PRO_TAG = 24 * 60 * 60 * 1000;

const vor = new Date(2026, 2, 28);
const nach = new Date(2026, 2, 30);

console.log((nach - vor) / MS_PRO_TAG);
// 1.9583333333333333

In Europe/Berlin wird am 29. März 2026 die Uhr auf Sommerzeit gestellt, dieser Tag hat nur 23 Stunden. Wer die Zahl mit Math.floor abschneidet, bekommt 1, mit Math.round 2. Im Herbst kommt umgekehrt 2.0416666666666665 raus (24. bis 26. Oktober). Man kann Date also durchaus richtig bedienen, nur muss man vorher wissen, dass die Falle da ist.

Bei Temporal sagst du, was du eigentlich wissen willst. Zwei Kalendertage auseinander:

const von = Temporal.PlainDate.from("2026-03-28");
const bis = Temporal.PlainDate.from("2026-03-30");

console.log(von.until(bis).toString());                        // P2D
console.log(von.until(bis, { largestUnit: "day" }).days);      // 2

Und wenn du mit echten Uhrzeiten in einer Zone rechnest, nimmst du ZonedDateTime. Das kennt die Sommerzeit:

const z = Temporal.ZonedDateTime.from("2026-03-28T12:00[Europe/Berlin]");

console.log(z.add({ days: 1 }).toString());
// 2026-03-29T12:00:00+02:00[Europe/Berlin]

console.log(z.add({ hours: 24 }).toString());
// 2026-03-29T13:00:00+02:00[Europe/Berlin]

console.log(z.until(z.add({ days: 1 })).toString());
// PT23H

console.log(Temporal.ZonedDateTime.from("2026-03-29T00:00[Europe/Berlin]").hoursInDay);
// 23

„Einen Tag später“ und „24 Stunden später“ sind hier zwei verschiedene Dinge, und bei Temporal schreibst du hin, welches du meinst. Date kennt nur die Millisekunden und die Setter. Mit Date lässt sich das an derselben Stelle nachstellen. Mitternacht am 29. März plus setDate(d.getDate() + 1) ergab bei mir den 30. März um 00:00, plus 24 Stunden in Millisekunden den 30. März um 01:00.

Falle 5: Das Monatsende läuft über

Der 31. Januar plus ein Monat. Einen 31. Februar gibt es nicht, irgendwas muss also passieren, und bei Date ist das der Überlauf:

const d = new Date(2026, 0, 31);
d.setMonth(d.getMonth() + 1);
console.log(d.toDateString()); // Tue Mar 03 2026

const d2 = new Date(2024, 0, 31);
d2.setMonth(d2.getMonth() + 1);
console.log(d2.toDateString()); // Sat Mar 02 2024

Aus dem 31. Februar wird der 3. März, in einem Schaltjahr der 2. März. Bei einer Abo-Verlängerung oder einer Zahlungsfrist ist das ärgerlich, denn das Monatsende liegt dann im nächsten Monat.

Temporal stutzt auf den letzten gültigen Tag zurecht (das ist wieder overflow: "constrain"):

console.log(Temporal.PlainDate.from("2026-01-31").add({ months: 1 }).toString());
// 2026-02-28

console.log(Temporal.PlainDate.from("2024-01-31").add({ months: 1 }).toString());
// 2024-02-29

console.log(Temporal.PlainDate.from("2026-01-31").add({ months: 2 }).toString());
// 2026-03-31

Ob du das magst, hängt vom Fall ab. Willst du lieber einen Fehler als ein stilles Zurechtstutzen, geht das auch:

Temporal.PlainDate.from("2026-01-31").add({ months: 1 }, { overflow: "reject" });
// RangeError

Bei Temporal steht das im Code. Bei Date merkt man es am 3. März.

Welcher Temporal-Typ wofür?

Date ist ein einziger Typ für alles: Zeitpunkt, Kalendertag, Uhrzeit. Temporal teilt das auf, und am Anfang fühlt sich das nach Papierkram an. Ich finde, die Typen stellen nur die Fragen, die man sich bei Date auch stellen müsste. Dort fragt halt keiner.

TypWas drinstecktBeispiel
PlainDateKalendertag, keine Uhrzeit, keine ZoneGeburtstag, Frist, Rechnungsdatum
PlainTimeUhrzeit, kein Datum, keine ZoneÖffnungszeit „09:00“
PlainDateTimeDatum und Uhrzeit, aber keine ZoneTermin im Wandkalender, Ort noch offen
ZonedDateTimeDatum, Uhrzeit und ZeitzoneMeeting um 14:30 in Berlin
Instantein Zeitpunkt, für alle gleichcreated_at, Logeintrag
Durationeine Zeitspanne„1 Stunde 30 Minuten“

Meine Faustregel: Steht in der Anforderung ein Ort oder eine Zone, nehme ich ZonedDateTime. Steht dort ein Tag („bis zum 15.“), reicht PlainDate. Und was gespeichert wird und später für alle dasselbe heißen soll, ist ein Instant. Alles andere sortiere ich erst, wenn es auffällt.

So sehen die Typen aus:

Temporal.PlainDate.from("2026-08-03").toString();
// 2026-08-03

Temporal.PlainTime.from("14:30").toString();
// 14:30:00

Temporal.PlainDateTime.from("2026-08-03T14:30").toString();
// 2026-08-03T14:30:00

const z = Temporal.ZonedDateTime.from("2026-08-03T14:30[Europe/Berlin]");
z.toString();
// 2026-08-03T14:30:00+02:00[Europe/Berlin]

z.withTimeZone("America/New_York").toString();
// 2026-08-03T08:30:00-04:00[America/New_York]

const i = Temporal.Instant.from("2026-08-03T12:30:00Z");
i.toZonedDateTimeISO("Europe/Berlin").toString();
// 2026-08-03T14:30:00+02:00[Europe/Berlin]

const dauer = Temporal.Duration.from({ hours: 1, minutes: 30 });
dauer.toString();         // PT1H30M
dauer.total("minutes");   // 90

Für Bestandscode gibt es eine Brücke: new Date("2026-08-03T12:30:00Z").toTemporalInstant() liefert einen Instant (2026-08-03T12:30:00Z). Du musst also nicht alles auf einmal umbauen, sondern kannst an den Rändern anfangen.

Beim Formatieren ändert sich wenig. Temporal-Objekte haben toLocaleString. Intl.DateTimeFormat nimmt die Plain-Typen auch direkt, ein ZonedDateTime aber nicht: format() wirft dort einen TypeError, da nimmst du toLocaleString oder vorher toPlainDateTime().

z.toLocaleString("de-DE", { dateStyle: "full", timeStyle: "short" });
// Montag, 3. August 2026 um 14:30

Temporal.PlainDate.from("2026-08-03").toLocaleString("de-DE", { dateStyle: "long" });
// 3. August 2026

Kann ich Temporal heute schon nehmen?

Kommt drauf an, wo der Code läuft.

Auf dem Server: In Node 26 ja, dort ist Temporal ohne Flag da. Ich habe es in node:26 ausprobiert, typeof Temporal ist dort "object". Node 24 hat es nicht.

Im Browser: Laut caniuse.com/temporal können Chrome und Edge es ab Version 144 (Januar 2026), Firefox ab Version 139 (Mai 2025). Safari hat es bisher nur in der Technology Preview, im stabilen Safari und auf iOS fehlt es. Weltweit deckt das laut caniuse rund 70 Prozent der Browsernutzung ab (Stand Oktober 2026). Bei knapp jedem dritten Besucher fehlt es also, und auf einer öffentlichen Seite geht das nicht ohne Polyfill.

Der Polyfill: @js-temporal/polyfill liegt auf npm. Mit Version 0.5.1 kam beim Monatsende aus Falle 5 dasselbe 2026-02-28 raus wie nativ. Ein Feature-Check, der mit und ohne natives Temporal läuft:

const T =
  globalThis.Temporal ?? (await import("@js-temporal/polyfill")).Temporal;

console.log(T.PlainDate.from("2026-01-31").add({ months: 1 }).toString());
// 2026-02-28

In node:26 kommt das native Objekt, in node:24 der Polyfill, und beide geben 2026-02-28 aus. Der Polyfill wird nur geladen, wenn er gebraucht wird. Im Browser geht derselbe Check in einem Modul, den nackten Paketnamen löst dort aber erst ein Bundler wie Vite auf.

Im Backend hängt nichts an Safari, dort ist der Einstieg am leichtesten. Im Frontend schaue ich erst, wie viel Date-Rechnerei dort wirklich steckt. Oft ist es ein toLocaleDateString und sonst nichts, und dafür brauchst du weder Temporal noch einen Polyfill.

Node 26 wird LTS: Was beim Upgrade sonst noch zählt

Temporal ist nur das, was mich am meisten interessiert. Node 26 bringt V8 14.6 mit, und der Release-Post führt ein paar Breaking Changes auf, die ich einmal gegen mein eigenes Projekt halten würde:

  • http.Server.prototype.writeHeader() ist entfernt. Der Ersatz ist writeHead().
  • Die Legacy-Module _stream_wrap, _stream_readable, _stream_writable, _stream_duplex, _stream_transform und _stream_passthrough sind entfernt.
  • Das Flag --experimental-transform-types ist entfernt.
  • module.register() ist zur Laufzeit deprecated.
  • localStorage gibt ohne Datei undefined zurück. In meinem Test im node:26-Container war typeof localStorage tatsächlich undefined, dazu kam eine ExperimentalWarning auf stderr.

Viel ist das nicht. Ich suche einmal per grep nach den Namen und rechne nicht damit, was zu finden.

Und es eilt auch nicht. Node 24 („Krypton“) ist laut Releaseplan seit dem 20. Oktober 2026 in der Maintenance-Phase und wird bis zum 30. April 2028 unterstützt. Node 26 ist ab dem 28. Oktober 2026 LTS, bis zum 30. April 2029. Wer zum Jahreswechsel umsteigt oder erst im Sommer, hat nichts verpasst. Wer Node gerade erst lernt, findet den Einstieg im Node.js-Kurs.

Und Date?

Temporal wird bei mir in neuem Code Standard. Date verschwindet trotzdem nicht, es steckt in jedem Bestandsprojekt und in jedem Schnipsel aus einer alten Forenantwort, den man irgendwann kopiert hat. Fehlt in altem Code ein Tag, schaue ich zuerst nach Falle 2 und 4.

Wenn du Date von Grund auf durchgehen willst, mit Editor zum Ausprobieren, gibt es dazu die Lektion Datum und Zeit im JavaScript-Kurs auf der Lernplattform.

Ob du Temporal jetzt schon einsetzt, ist deine Sache. Ich fange bei den Stellen an, an denen ich zuletzt über einen verschobenen Tag gestolpert bin, und die liegen bei mir im Backend. Der Rest kann bleiben, wie er ist.

Newsletter

einmal die Woche, kostenlos

Neue Artikel und Videos, was sich auf der Lernplattform tut, und ab und zu ein Sonderpreis zum Kursstart. Kurz genug, dass du es auch liest.

Zuletzt verschickt · KW 44

Der Python-Kurs ist online

Danach kommt eine Mail mit Bestätigungslink, erst dann bist du eingetragen. Abmelden geht unten in jeder Ausgabe. Mehr dazu in der Datenschutzerklärung.