Dowiedz się, jak formatować i konwertować czas w JavaScript bez niespodzianek: znaczniki czasu, ciągi ISO, strefy czasowe, DST, zasady parsowania i sprawdzone wzorce.

Błędy związane z czasem w JavaScript rzadko wyglądają jak „zegar jest zepsuty”. Pojawiają się jako mylące przesunięcia: data poprawna na twoim laptopie, ale nie na komputerze kolegi, odpowiedź API, która wydaje się prawidłowa, dopóki nie zostanie wyrenderowana w innej strefie czasowej, albo raport „o dzień w bok” wokół zmiany sezonowej.
Zazwyczaj zauważysz jedno (lub więcej) z poniższych:
+02:00) jest inne niż oczekiwałeś.Dużym źródłem problemów jest to, że słowo czas może odnosić się do różnych pojęć:
Wbudowany Date w JavaScript próbuje obsłużyć wszystkie te przypadki, ale przede wszystkim reprezentuje moment w czasie, jednocześnie skłaniając cię do wyświetlania w czasie lokalnym, co ułatwia przypadkowe konwersje.
Ten przewodnik jest praktyczny: jak uzyskać przewidywalne konwersje między przeglądarkami i serwerami, jak wybierać bezpieczniejsze formaty (np. ISO 8601) i jak wykrywać klasyczne pułapki (sekundy vs milisekundy, UTC vs lokalny, różnice w parsowaniu). Celem nie jest teoria — lecz mniej pytań „dlaczego się przesunęło?”.
Błędy związane z czasem w JavaScript często zaczynają się od mieszania reprezentacji, które wyglądają na wymienne, ale takie nie są.
1) Milisekundy epoki (number)
Zwykła liczba jak 1735689600000 to zazwyczaj „milisekundy od 1970-01-01T00:00:00Z”. Reprezentuje moment w czasie bez formatu i bez strefy czasowej.
2) Obiekt Date (opakowanie wokół momentu)
Date przechowuje ten sam typ informacji co znacznik czasu. Mylące jest to, że gdy drukujesz Date, JavaScript formatuje go używając lokalnych reguł środowiska, o ile nie poprosisz inaczej.
3) Sformatowany ciąg (do wyświetlenia)
Ciągi jak "2025-01-01", "01/01/2025 10:00" czy "2025-01-01T00:00:00Z" to różne rzeczy. Niektóre są jednoznaczne (ISO 8601 z Z), inne zależą od lokalizacji, a niektóre nie zawierają strefy czasowej wcale.
Ten sam moment może być wyświetlany inaczej w różnych strefach czasowych:
const instant = new Date("2025-01-01T00:00:00Z");
instant.toLocaleString("en-US", { timeZone: "UTC" });
// "1/1/2025, 12:00:00 AM"
instant.toLocaleString("en-US", { timeZone: "America/Los_Angeles" });
// "12/31/2024, 4:00:00 PM" (poprzedni dzień)
Wybierz jedną wewnętrzną reprezentację (często milisekundy epoki lub UTC ISO 8601) i trzymaj się jej w całej aplikacji i API. Konwertuj do/z Date i sformatowanych ciągów tylko na granicach: przy parsowaniu wejścia i przy wyświetlaniu w UI.
„Znacznik czasu” zwykle oznacza czas epoki (Unix time): liczbę sekund/miilisekund od 1970-01-01 00:00:00 UTC. Pułapka: różne systemy liczą w różnych jednostkach.
Date w JavaScript powoduje najwięcej nieporozumień, bo używa milisekund. Wiele API, baz danych i logów używa sekund.
17040672001704067200000Ten sam moment, ale wersja w milisekundach ma trzy dodatkowe cyfry.
Używaj explicite mnożenia/dzielenia, żeby jednostka była oczywista:
// seconds -> Date
const seconds = 1704067200;
const d1 = new Date(seconds * 1000);
// milliseconds -> Date
const ms = 1704067200000;
const d2 = new Date(ms);
// Date -> seconds
const secondsOut = Math.floor(d2.getTime() / 1000);
// Date -> milliseconds
const msOut = d2.getTime();
Date()To wygląda rozsądnie, ale jest błędne, gdy ts są w sekundach:
const ts = 1704067200; // seconds
const d = new Date(ts); // WRONG: treated as milliseconds
Wynik będzie datą w 1970, ponieważ 1,704,067,200 milisekund to tylko około 19 dni po epoce.
Kiedy nie jesteś pewny, jakiej jednostki masz, dodaj proste zabezpieczenia:
function asDateFromUnknownEpoch(x) {
// crude heuristic: seconds are ~1e9-1e10, milliseconds are ~1e12-1e13
if (x < 1e11) return new Date(x * 1000); // assume seconds
return new Date(x); // assume milliseconds
}
const input = Number(valueFromApi);
console.log({ input, digits: String(Math.trunc(input)).length });
console.log('as ISO:', asDateFromUnknownEpoch(input).toISOString());
Jeśli liczba cyfr to ~10, prawdopodobnie są to sekundy. Jeśli ~13, to milisekundy. Drukowanie toISOString() podczas debugowania szybko ujawnia błędy jednostek.
Date w JavaScript może być mylący, bo przechowuje pojedynczy moment w czasie, ale może prezentować ten moment w różnych strefach czasowych.
Wewnątrz Date to w zasadzie „milisekundy od epoki Unix (1970-01-01T00:00:00Z)”. Ta liczba reprezentuje moment w UTC. „Przesunięcie” pojawia się, gdy prosisz JavaScript o sformatowanie tego momentu jako czas lokalny (na podstawie ustawień komputera/serwera) zamiast UTC.
Wiele API Date ma zarówno warianty lokalne, jak i UTC. Zwracają różne liczby dla tego samego momentu:
const d = new Date('2025-01-01T00:30:00Z');
d.getHours(); // hour in *local* time zone
d.getUTCHours(); // hour in UTC
d.toString(); // local time string
d.toISOString(); // UTC (always ends with Z)
Jeśli twój komputer jest w Nowym Jorku (UTC-5), ten czas UTC może pojawić się lokalnie jako „19:30” poprzedniego dnia. Na serwerze ustawionym na UTC będzie to „00:30”. Ten sam moment, różne wyświetlenie.
Logi często używają Date#toString() lub interpolują Date implicite, co używa lokalnej strefy czasowej środowiska. To oznacza, że ten sam kod może wydrukować różne znaczniki czasu na twoim laptopie, w CI i w produkcji.
Przechowuj i przesyłaj czas w UTC (np. ms epoki lub ISO 8601 z Z). Konwertuj na strefę użytkownika tylko przy wyświetlaniu:
toISOString() albo przesyłaj milisekundy epokiIntl.DateTimeFormatJeśli szybko tworzysz aplikację (np. w workflow vibe-coding w Koder.ai), warto wcześnie ustalić kontrakty API: nazwij pola wprost (createdAtMs, createdAtIso) i trzymaj spójność między serwerem (Go + PostgreSQL) a klientem (React).
Jeśli musisz wysyłać daty/czasy między przeglądarką, serwerem i bazą danych, ciągi ISO 8601 to najbezpieczniejszy wybór. Są jawne, szeroko wspierane i (co najważniejsze) niosą informacje o strefie czasowej.
Dwa dobre formaty wymiany:
2025-03-04T12:30:00Z2025-03-04T12:30:00+02:00Co oznacza „Z”?
Z to skrót od Zulu time, inna nazwa dla UTC. Więc 2025-03-04T12:30:00Z to „12:30 w UTC”.
Kiedy offsety jak +02:00 mają znaczenie?
Offsety są kluczowe, gdy zdarzenie jest powiązane z kontekstem lokalnej strefy (wizyty, rezerwacje, godziny otwarcia). 2025-03-04T12:30:00+02:00 opisuje moment dwie godziny przed UTC, i to nie jest ten sam moment co 2025-03-04T12:30:00Z.
Ciągi jak 03/04/2025 to pułapka: czy to 4 marca czy 3 kwietnia? Różni użytkownicy i środowiska interpretują to inaczej. Preferuj 2025-03-04 (data w ISO) lub pełny datetime w ISO.
const iso = "2025-03-04T12:30:00Z";
const d = new Date(iso);
const back = d.toISOString();
console.log(iso); // 2025-03-04T12:30:00Z
console.log(back); // 2025-03-04T12:30:00.000Z
To „round-trip” to dokładnie to, czego chcesz dla API: spójne, przewidywalne i świadome strefy czasowej.
Date.parse() wydaje się wygodne: podaj ciąg, dostaniesz znacznik czasu. Problem w tym, że dla wszystkiego poza jednoznacznym ISO 8601 parsowanie może zależeć od heurystyk przeglądarki. Te heurystyki różniły się między silnikami i wersjami, co oznacza, że ten sam input może zostać sparsowany inaczej (lub wcale) w zależności od środowiska.
Date.parse() może się różnićJavaScript standaryzuje parsowanie pewnych formatów ISO 8601 (i nawet wtedy szczegóły strefy czasowej mają znaczenie). Dla „przyjaznych” formatów — jak "03/04/2025", "March 4, 2025" czy "2025-3-4" — przeglądarki mogą interpretować:
Jeśli nie możesz przewidzieć dokładnego kształtu ciągu, nie możesz przewidzieć wyniku.
YYYY-MM-DDPowszechna pułapka to prosty format "YYYY-MM-DD" (np. "2025-01-15"). Wielu programistów oczekuje, że będzie traktowany jako lokalne północ. W praktyce niektóre środowiska traktują taką formę jako UTC midnight.
Ta różnica ma znaczenie: północ UTC przekonwertowana na czas lokalny może stać się poprzednim dniem w strefach ujemnych (np. Ameryki) lub przesunąć godzinę. To łatwy sposób na pojawienie się błędu „dlaczego moja data jest przesunięta o jeden dzień?”.
Dla wejścia do serwera/API:
2025-01-15T13:45:00Z lub 2025-01-15T13:45:00+02:00."YYYY-MM-DD") i unikaj konwersji na Date, chyba że zdefiniujesz zamierzoną strefę czasową.Dla wejścia od użytkownika:
03/04/2025, chyba że UI narzuca znaczenie.Zamiast polegać na Date.parse(), aby „domyślić się”, wybierz jedną z tych strategii:
new Date(year, monthIndex, day) dla dat lokalnych).Gdy dane czasowe są krytyczne, „u mnie parsuje” nie wystarczy — zdefiniuj reguły parsowania jawnie i konsekwentnie.
Jeśli celem jest „pokazać datę/czas tak, jak ludzie tego oczekują”, najlepszym narzędziem w JavaScript jest Intl.DateTimeFormat. Używa reguł lokalnych użytkownika (kolejność, separatory, nazwy miesięcy) i unika kruchego składania ciągów jak month + '/' + day.
Ręczne formatowanie często twardo koduje amerykański styl, zapomina o zerach wiodących lub daje mylący efekt 24/12 godzin. Intl.DateTimeFormat pozwala też jawnie określić, w której strefie czasowej wyświetlasz — kluczowe, gdy dane przechowywane są w UTC, a UI ma pokazywać czas użytkownika.
Dla „ładnego” formatowania dateStyle i timeStyle są najprostsze:
const d = new Date('2025-01-05T16:30:00Z');
// User’s locale + user’s local time zone
console.log(new Intl.DateTimeFormat(undefined, {
dateStyle: 'medium',
timeStyle: 'short'
}).format(d));
// Force a specific time zone (great for event times)
console.log(new Intl.DateTimeFormat('en-GB', {
dateStyle: 'full',
timeStyle: 'short',
timeZone: 'UTC'
}).format(d));
Jeśli potrzebujesz spójnego cyklu godzin (np. ustawienie w preferencjach), użyj hour12:
console.log(new Intl.DateTimeFormat('en-US', {
hour: 'numeric',
minute: '2-digit',
hour12: true
}).format(d));
Wybierz jedną funkcję formatowania dla każdego „typu” znacznika czasu w UI (czas wiadomości, wpisu w logu, początek wydarzenia) i miej świadomą decyzję o timeZone:
To daje spójny, przyjazny użytkownikowi output bez utrzymywania kruchego zestawu własnych formatów.
Czas letni (DST) to moment, gdy strefa czasowa zmienia swoje przesunięcie UTC (zwykle o jedną godzinę) w określonych datach. Trudność polega na tym, że DST nie tylko „zmienia przesunięcie” — zmienia też istnienie pewnych lokalnych godzin.
Gdy zegary przyspieszają (spring forward), zakres lokalnych godzin nigdy nie występuje. Na przykład zegar może przeskoczyć z 01:59 do 03:00, więc 02:30 lokalnego czasu nie ma.
Gdy zegary cofają się (fall back), zakres godzin występuje dwukrotnie. Na przykład 01:30 może wystąpić raz przed przesunięciem i raz po, co oznacza, że ta sama godzina wskazówkowa może odnosić się do dwóch różnych momentów.
To nie to samo przy granicach DST:
Jeśli dziś jest rozpoczęcie DST, „jutro o 9:00” może być tylko 23 godziny dalej. Jeśli DST się zakończy, może to być 25 godzin.
// Scenario: schedule “same local time tomorrow”
const d = new Date(2025, 2, 8, 9, 0); // Mar 8, 9:00 local
const plus24h = new Date(d.getTime() + 24 * 60 * 60 * 1000);
const nextDaySameLocal = new Date(d);
nextDaySameLocal.setDate(d.getDate() + 1);
// Around DST, plus24h and nextDaySameLocal can differ by 1 hour.
setHours może cię zaskoczyćJeśli zrobisz coś w stylu date.setHours(2, 30, 0, 0) w dniu „spring forward”, JavaScript może znormalizować to do innego ważnego czasu (często 03:30), ponieważ 02:30 nie istnieje w lokalnym czasie.
setDate) zamiast dodawania milisekund.Z, żeby moment był jednoznaczny.Częstym źródłem błędów jest używanie Date do reprezentowania czegoś, co nie jest momentem kalendarzowym.
Timestamp odpowiada na pytanie „kiedy to się stało?” (konkretny moment jak 2025-12-23T10:00:00Z). Duration odpowiada na „jak długo?” (np. „3 minuty 12 sekund”). To różne pojęcia i mieszanie ich prowadzi do mylących obliczeń i niespodzianek związanych ze strefą czasową/DST.
Date to zły wybór dla durationDate zawsze reprezentuje punkt na osi czasu względem epoki. Jeśli przechowasz „90 sekund” jako Date, tak naprawdę przechowujesz „1970-01-01 plus 90 sekund” w określonej strefie. Formatowanie może wtedy pokazać 01:01:30, przesunąć się o godzinę lub pokazać datę, której nie zamierzałeś.
Dla duration lepiej używać zwykłych liczb:
HH:mm:ssProsty formatter, który działa dla timerów i długości mediów:
function formatHMS(totalSeconds) {
const s = Math.max(0, Math.floor(totalSeconds));
const hh = String(Math.floor(s / 3600)).padStart(2, "0");
const mm = String(Math.floor((s % 3600) / 60)).padStart(2, "0");
const ss = String(s % 60).padStart(2, "0");
return `${hh}:${mm}:${ss}`;
}
formatHMS(75); // "00:01:15" (countdown timer)
formatHMS(5423); // "01:30:23" (media duration)
Jeśli konwertujesz z minut, najpierw mnoż przez 60 (minutes * 60) i trzymaj wartość jako liczbę aż do renderowania.
Gdy porównujesz czasy w JavaScript, najbezpieczniej porównywać liczby, nie sformatowany tekst. Obiekt Date jest opakowaniem wokół liczby (milisekundy epoki), więc porównania powinny sprowadzać się do „liczba vs liczba”.
Używaj getTime() (lub Date.valueOf(), który zwraca tę samą liczbę), aby porównywać niezawodnie:
const a = new Date('2025-01-10T12:00:00Z');
const b = new Date('2025-01-10T12:00:01Z');
if (a.getTime() < b.getTime()) {
// a is earlier
}
// Also works:
if (+a < +b) {
// unary + calls valueOf()
}
Unikaj porównywania sformatowanych ciągów jak "1/10/2025, 12:00 PM" — są zależne od lokalizacji i nie będą poprawnie sortować. Głównym wyjątkiem są ciągi ISO 8601 w tym samym formacie i strefie (np. wszystkie ...Z), które sortują się leksykograficznie.
Sortowanie po czasie jest proste, jeśli sortujesz po milisekundach epoki:
items.sort((x, y) => new Date(x.createdAt).getTime() - new Date(y.createdAt).getTime());
Filtrowanie elementów w zakresie to ta sama idea:
const start = new Date('2025-01-01T00:00:00Z').getTime();
const end = new Date('2025-02-01T00:00:00Z').getTime();
const inRange = items.filter(i => {
const t = new Date(i.createdAt).getTime();
return t >= start && t < end;
});
„Początek dnia” zależy od tego, czy masz na myśli czas lokalny czy UTC:
// Local start/end of day
const d = new Date(2025, 0, 10); // Jan 10 in local time
const localStart = new Date(d.getFullYear(), d.getMonth(), d.getDate(), 0, 0, 0, 0);
const localEnd = new Date(d.getFullYear(), d.getMonth(), d.getDate(), 23, 59, 59, 999);
// UTC start/end of day
const utcStart = new Date(Date.UTC(2025, 0, 10, 0, 0, 0, 0));
const utcEnd = new Date(Date.UTC(2025, 0, 10, 23, 59, 59, 999));
Wybierz jedną definicję wcześnie i trzymaj się jej w porównaniach i logice zakresów.
Błędy czasowe wydają się losowe, dopóki nie wyznaczysz czego dokładnie masz (znacznik czasu? ciąg? Date?) i gdzie wprowadzono przesunięcie (parsowanie, konwersja strefy, formatowanie).
Zacznij od logowania tej samej wartości na trzy różne sposoby. To szybko pokaże, czy problem to sekundy vs milisekundy, lokalne vs UTC, czy parsowanie ciągu.
console.log('raw input:', input);
const d = new Date(input);
console.log('toISOString (UTC):', d.toISOString());
console.log('toString (local):', d.toString());
console.log('timezone offset (min):', d.getTimezoneOffset());
Na co zwrócić uwagę:
toISOString() jest bardzo „od czapy” (np. rok 1970 lub bardzo daleka przyszłość), podejrzewaj sekundy vs milisekundy.toISOString() wygląda dobrze, ale toString() jest „przesunięty”, widzisz wyświetlanie w strefie lokalnej.getTimezoneOffset() zmienia się w zależności od daty, przekraczasz czas letni.Wiele „u mnie działa” to po prostu różne domyślne ustawienia środowiska.
console.log(Intl.DateTimeFormat().resolvedOptions());
console.log('TZ:', process.env.TZ);
console.log(Intl.DateTimeFormat().resolvedOptions().timeZone);
Jeśli twój serwer działa w UTC, a laptop w strefie lokalnej, sformatowane wyjście będzie inne, chyba że jawnie określisz timeZone.
Stwórz testy jednostkowe wokół granic DST i „krawędziowych” czasów:
23:30 → 00:30Jeśli szybko iterujesz, rozważ włączenie tych testów do scaffoldu. Na przykład przy generowaniu React + Go app w Koder.ai możesz dodać mały zestaw testów „time contract” od razu (przykładowe payloady API + asercje parsowania/formatowania), żeby regresje były łapane przed deploymentem.
"2025-03-02 10:00".locale i (gdy potrzeba) timeZone.Niezawodna obsługa czasu w JavaScript to w dużej mierze wybór „źródła prawdy” i konsekwencja od przechowywania do wyświetlania.
Przechowuj i obliczaj w UTC. Traktuj lokalny czas użytkownika jako szczegół prezentacji.
Przesyłaj daty między systemami jako ciągi ISO 8601 z jawnie podanym offsetem (najlepiej Z). Jeśli musisz wysyłać epoki liczbowe, udokumentuj jednostkę i trzymaj ją spójną (milisekundy to domyślna i powszechna opcja w JS).
Formatuj dla ludzi za pomocą Intl.DateTimeFormat (lub toLocaleString) i podawaj jawny timeZone, gdy potrzebujesz deterministycznego wyjścia (np. zawsze pokazywać czasy w UTC lub w konkretnej strefie biznesowej).
Z (np. 2025-12-23T10:15:00Z). Jeśli używasz epok, dodaj nazwę pola jak createdAtMs, żeby jednostki były jasne.Rozważ dedykowaną bibliotekę daty-czasu, jeśli potrzebujesz zdarzeń cyklicznych, złożonych reguł stref czasowych, arytmetyki bezpiecznej względem DST („ta sama godzina jutro”) lub dużej ilości parsowania niespójnych wejść. Wartość to czytelniejsze API i mniej edge-case'ów.
Jeśli chcesz zgłębić temat, przejrzyj więcej przewodników o czasie pod tekstem „/blog”. Jeśli oceniasz narzędzia lub opcje wsparcia, zobacz „/pricing”.