Praca z datami w JavaScript wygląda prosto tylko do pierwszej strefy czasowej, zmiany czasu albo porównania dwóch momentów zapisanych w różnych formatach. W tym tekście pokazuję, jak działa obiekt Date, jak go tworzyć i formatować, gdzie najczęściej pojawiają się błędy oraz kiedy lepiej myśleć w UTC, a kiedy w czasie lokalnym. To praktyczny przewodnik dla osób, które chcą pisać kod odporny na kalendarzowe niespodzianki.
Najważniejsze zasady pracy z datami w JavaScript
-
Dateprzechowuje moment jako timestamp w milisekundach od epoki UTC, a metody bez prefiksu UTC zwykle czytają czas lokalny. -
getMonth()zwraca zakres 0-11, więc styczeń to 0, a nie 1. -
Date.parse()najlepiej ograniczać do przewidywalnych formatów ISO 8601, bo nietypowe stringi bywają nieprzenośne. -
Do prezentacji dla użytkownika używaj
Intl.DateTimeFormat, najlepiej z właściwą lokalizacją i strefą. - W danych i API trzymaj UTC, a dopiero na końcu zamieniaj wynik na format czytelny dla człowieka.
-
Temporal rozwiązuje wiele bolączek
Date, ale wciąż warto sprawdzić dostępność w środowisku projektu.
Jak naprawdę działa obiekt Date i dlaczego łatwo się na nim przejechać
Jak opisuje MDN, Date to w praktyce przede wszystkim liczba milisekund od epoki, czyli od północy 1 stycznia 1970 roku w UTC. To ważne, bo sam obiekt nie przechowuje „magicznie” daty w stylu kalendarza, tylko jeden konkretny moment w czasie. Dopiero metody bez prefiksu UTC odczytują go przez pryzmat lokalnej strefy systemu, na którym działa kod.
Ja zwykle myślę o tym tak: wewnątrz masz timestamp, na zewnątrz możesz dostać rok, miesiąc, dzień albo godzinę, ale sposób interpretacji zależy od metody. To dlatego ten sam obiekt może wyświetlić inną godzinę na serwerze niż w przeglądarce użytkownika. Warto też pamiętać, że nie każda „data” jest poprawna. Jeśli parser nie zrozumie wejścia, dostajesz obiekt z wartością niepoprawną, a przy próbie serializacji do ISO możesz zobaczyć błąd typu RangeError.
const teraz = new Date();
const poczatekEpoki = new Date(0);
const ms = teraz.getTime();
Ten model jest prosty, ale ma swoją cenę: jeśli pomylisz moment czasu z datą kalendarzową, błąd potrafi wyglądać wiarygodnie aż do momentu wdrożenia. Skoro wiemy już, co siedzi pod maską, przejdźmy do tworzenia dat tak, by nie dokładać sobie niepotrzebnego chaosu.
Jak tworzyć daty bez niejasności
Najbezpieczniej zaczynać od pytania, czy tworzysz konkretny moment w czasie, czy po prostu kalendarzową datę. W JavaScript do dyspozycji masz kilka dróg, ale nie wszystkie są równie dobre. new Date() bierze bieżący moment, a konstruktor z argumentami liczbowymi tworzy datę w lokalnej strefie systemu, co bywa wygodne, ale nie zawsze oczywiste.
Najbardziej przewidywalny wariant to jawny zapis ISO 8601 z oznaczeniem strefy, na przykład 2026-06-30T12:00:00Z. Jeśli budujesz datę z części, a chcesz uniknąć zależności od lokalnej strefy, użyj Date.UTC() i dopiero potem opakuj wynik w Date. To pozwala jasno powiedzieć: ten czas jest liczony w UTC, nie „na oko” według ustawień komputera.
const teraz = new Date();
const lokalnaData = new Date(2026, 0, 15, 10, 30); // 15 stycznia 2026, 10:30 czasu lokalnego
const utcData = new Date(Date.UTC(2026, 0, 15, 10, 30)); // ten sam układ pól, ale w UTC
const isoData = new Date("2026-01-15T10:30:00Z");
W praktyce unikam też luźnych stringów w stylu 31/12/2026, bo taki zapis jest czytelny dla człowieka, ale dla parsera bywa nieprzenośny. Date.parse() najlepiej traktować ostrożnie: dla formatów poza standardem zachowanie może zależeć od środowiska. Jeśli muszę sprawdzić poprawność wejścia, robię to jawnie:
const data = new Date(input);
if (Number.isNaN(data.getTime())) {
// wejście jest niepoprawne
}
Gdy data już istnieje, zwykle trzeba z niej coś odczytać albo ją przesunąć, i wtedy zaczynają się pierwsze pułapki.
Jak odczytywać i zmieniać składniki daty
W codziennej pracy najczęściej rozbijam datę na rok, miesiąc, dzień i godzinę. Tu najważniejsze pytanie brzmi: czy interesuje mnie czas lokalny, czy UTC? Jeśli pokazuję wynik użytkownikowi, zwykle biorę metody lokalne. Jeśli porównuję dane między systemami albo przetwarzam logi, częściej sięgam po wersje UTC.
| Zadanie | Lokalnie | W UTC |
|---|---|---|
| Rok | getFullYear() |
getUTCFullYear() |
| Miesiąc | getMonth() |
getUTCMonth() |
| Dzień miesiąca | getDate() |
getUTCDate() |
| Godzina | getHours() |
getUTCHours() |
| Minuty | getMinutes() |
getUTCMinutes() |
Najczęstsza wpadka to miesiące. getMonth() zwraca wartości od 0 do 11, więc styczeń to 0, luty to 1, a grudzień to 11. To jeden z tych detali, które człowiek pamięta niechętnie, ale potem płaci za nie godziną debugowania. Druga klasyka to pomylenie getDate() z getDay(): pierwsza metoda zwraca dzień miesiąca, druga dzień tygodnia.
const data = new Date(2026, 0, 31);
data.setMonth(1); // luty nie ma 31 dni, więc wynik może przesunąć się dalej
Właśnie dlatego przy obliczaniu czasu trwania używam różnicy timestampów, a nie ręcznego odejmowania pól. Jeśli chcesz policzyć, ile minęło milisekund, sekund albo dni, bazuj na liczbie od epoki, nie na rozbitych składnikach. To nadal nie rozwiązuje jednak najważniejszej części problemu, czyli tego, jak pokazać datę człowiekowi tak, żeby format był czytelny i zgodny z językiem interfejsu.
Jak formatować daty dla użytkownika po polsku
W warstwie prezentacji nie składam dat ręcznie z kawałków tekstu. Po prostu używam Intl.DateTimeFormat, bo to narzędzie jest stworzone do formatowania zależnego od języka, regionu i strefy czasowej. Dla polskiego interfejsu najczęściej wybieram pl-PL i ustawiam odpowiednią strefę, zamiast zakładać, że domyślne ustawienia środowiska „jakoś” wystarczą.
const formatter = new Intl.DateTimeFormat("pl-PL", {
dateStyle: "medium",
timeStyle: "short",
timeZone: "Europe/Warsaw",
});
console.log(formatter.format(new Date()));
Jeśli potrzebuję własnego układu elementów, sięgam po formatToParts() zamiast składać datę ręcznie. To daje mi kontrolę nad kolejnością składników, ale nadal korzysta z reguł lokalizacji. W praktyce wygląda to znacznie lepiej niż ręczne dodawanie zer w miesiącu czy dniu.
| Metoda | Kiedy używać | Ograniczenie |
|---|---|---|
toISOString() |
API, logi, zapis maszynowy | Zawsze UTC, nie do interfejsu użytkownika |
toLocaleDateString("pl-PL") |
Szybki pokaz samej daty | Mniejsza kontrola nad detalami |
Intl.DateTimeFormat |
W pełni świadome formatowanie w UI | Warto jawnie ustawić locale i strefę |
formatToParts() |
Własny układ etykiet i komponentów | Wymaga trochę więcej kodu |
Sam dobry format nie rozwiązuje jeszcze błędów związanych z czasem letnim i przesunięciami stref, więc właśnie tam najłatwiej o pozornie „losowe” pomyłki.

Dlaczego strefy czasowe i czas letni psują proste założenia
Największy problem nie leży w samych datach, tylko w różnicy między momentem czasu a dniem kalendarzowym. Jeśli zapisujesz „12 marca o 10:00” jako zwykły timestamp, to przy innym ustawieniu strefy wynik może wyglądać inaczej, mimo że dane są technicznie poprawne. Do tego dochodzi czas letni: jeden dzień może mieć 23 godziny, a inny 25.
To praktyczny powód, dla którego nie lubię dodawać „po prostu 24 godzin” do daty, gdy chodzi o lokalny termin. 24 godziny to 86 400 000 milisekund i ta wartość jest stała, ale lokalny czas po zmianie strefy albo przejściu na czas letni może już taki nie być. Jeśli użytkownik oczekuje „następnego dnia o tej samej godzinie”, trzeba pracować na zasadach kalendarzowych, nie na surowej arytmetyce czasu.
- W bazie i API zapisuję UTC, bo to najstabilniejsza postać danych.
- W interfejsie formatuję czas dla użytkownika, najlepiej w jego strefie i języku.
- Dla wydarzeń związanych z konkretnym miejscem trzymam nazwę strefy, a nie sam offset, bo offset zmienia się sezonowo.
- Do liczenia czasu trwania używam różnicy timestampów, a nie sumowania pól daty.
Jeśli sprawdzam lokalny offset, pamiętam, że może się zmieniać w ciągu roku. To właśnie dlatego aplikacje rezerwacyjne, kalendarze i systemy rozliczeń tak często potykają się o niby banalne założenia. Na tym tle dobrze widać, po co w ogóle pojawił się Temporal i kiedy jego model daje przewagę.
Kiedy Date wystarczy, a kiedy lepiej spojrzeć na Temporal
Nie wyrzucałbym Date z projektu tylko dlatego, że istnieje nowsze API. Do prostego timestampu, logów, cache i zwykłych widoków nadal wystarczy. Problem zaczyna się wtedy, gdy aplikacja operuje na datach bez godziny, na wielu strefach albo na obliczeniach, które muszą być odporne na DST. Dokumentacja TC39 pokazuje, że Temporal adresuje właśnie te słabe punkty: niemutowalność, pełniejszą obsługę stref, typy dla dat bez godziny i lepszą ergonomię całego modelu.
| Sytuacja | Date |
Temporal |
|---|---|---|
| Zapis jednej chwili w czasie | Wystarcza | Wystarcza |
| Data bez godziny | Niewygodne | Naturalne |
| Operacje odporne na DST | Ryzykowne | Projektowane pod ten przypadek |
| Mutowalność obiektu | Tak | Nie |
| Dostępność w środowisku | Praktycznie wszędzie | Zależna od runtime lub polyfilla |
W 2026 Temporal jest już blisko dojrzałego standardu, więc przy nowych projektach traktuję go jako kierunek, który warto śledzić bardzo uważnie. Nie zmuszałbym jednak zespołu do przepisywania stabilnego kodu bez wyraźnego zysku. Jeśli problem jest prosty, Date plus Intl.DateTimeFormat nadal zrobią robotę. Jeśli problem jest złożony, Temporal zaczyna mieć dużo więcej sensu.
Na koniec zostawiam zestaw zasad, które ja stosuję najczęściej, gdy projekt ma po prostu działać i nie sprawiać przykrości po północy.
Jakie zasady wdrażam od razu w projekcie
-
Przechowuję momenty w UTC i zapisuję je w formacie ISO z
Z. - Dla dat kalendarzowych bez godziny rozdzielam dzień od timestampu, zamiast upychać wszystko w jednym polu.
-
W UI formatowanie robi
Intl.DateTimeFormat, a nie ręczne sklejanie stringów. -
Date.parse()używam tylko dla formatów, nad którymi mam kontrolę. -
getMonth()zawsze koryguję o+1, jeśli pokazuję wynik człowiekowi. - Do mierzenia czasu używam różnicy timestampów, nie pól daty.
To zestaw, który w większości aplikacji naprawdę wystarcza, żeby daty przestały zaskakiwać po wdrożeniu. Gdy projekt wchodzi w rezerwacje, planowanie, rozliczenia albo wiele stref czasowych naraz, wtedy lepiej świadomie przejść na Temporal lub precyzyjnie zaprojektować model danych, zamiast liczyć na kolejne obejście.