Научитесь форматировать и преобразовывать время в JavaScript без сюрпризов: метки времени, ISO-строки, часовые пояса, летнее время, правила парсинга и надёжные шаблоны.

Ошибки с временем в JavaScript редко выглядят как «часы показывают неправильно». Чаще это маленькие сбои: дата, корректная на вашем ноутбуке, но неверная на машине коллеги; ответ API, который выглядит нормально, пока его не отобразить в другой временной зоне; или отчёт, «сдвинутый на один» вокруг сезонного перехода времени.
Обычно вы заметите одно (или несколько) из следующих:
+02:00) отличается от ожидаемого.\n- Непоследовательное форматирование: «работает в Chrome» но выглядит иначе в Safari, или сервер и браузер по-разному парсят строку.Большая часть проблем в том, что слово время может обозначать разные концепции:
"2025-12-23T10:00:00Z"). Это то, что обычно нужно для логов, событий и хранения в API.\n- Календарная дата: день в календаре без часового пояса (например, день рождения, дата счёта). Обращение с ней как с моментом может сдвинуть её на соседний день.\n- Показанное локальное время (wall-clock time): «9:00 в Берлине», которое зависит от правил часового пояса и переходов на летнее/зимнее время.Встроенный Date в JavaScript пытается охватить всё это, но в основе представляет момент времени, при этом постоянно подталкивая вас к локальному отображению, что делает случайные преобразования лёгкими.
Это руководство предельно практично: как получить предсказуемые преобразования в браузерах и на серверах, как выбирать более безопасные форматы (например, ISO 8601) и как замечать классические ловушки (секунды против миллисекунд, UTC против локального, и различия парсинга). Цель не в теории — а в том, чтобы было меньше «почему это сдвинулось?» сюрпризов.
Ошибки с временем в JavaScript часто начинаются с смешивания представлений, которые выглядят взаимозаменяемыми, но таковыми не являются.
1) Эпоха в миллисекундах (число)
Простое число вроде 1735689600000 обычно означает «миллисекунды с 1970-01-01T00:00:00Z». Оно представляет момент времени без форматирования и часового пояса.
2) Объект Date (обёртка вокруг момента)
Date хранит тот же тип момента, что и метка времени. Путаница в том, что при выводе Date JavaScript форматирует его по локальным правилам окружения, если вы не указали иное.
3) Форматированная строка (для человека)
Строки вроде "2025-01-01", "01/01/2025 10:00" или "2025-01-01T00:00:00Z" — это не одно и то же. Некоторые однозначны (ISO 8601 с Z), другие зависят от локали, а некоторые вообще не содержат информации о часовом поясе.
Один и тот же момент может отображаться по-разному в разных часовых поясах:
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" (предыдущий день)
Выберите одно внутреннее представление (обычно миллисекунды эпохи или UTC ISO 8601) и придерживайтесь его по всему приложению и API. Конвертируйте в/из Date и форматированные строки только на границах: при парсинге входных данных и при отображении в UI.
«Метка времени» обычно означает эпоху (Unix time): отсчёт времени с 1970-01-01 00:00:00 UTC. Загвоздка в том, что разные системы считают в разных единицах.
Date в JavaScript — источник большинства недоразумений, потому что он использует миллисекунды. Многие API, БД и логи используют секунды.
1704067200\n- JavaScript timestamp (миллисекунды): 1704067200000Один и тот же момент, но версия в миллисекундах имеет три дополнительных цифры.
Используйте явное умножение/деление, чтобы явно обозначить единицу:
// 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()Это выглядит разумно, но неправильно, когда ts в секундах:
const ts = 1704067200; // seconds
const d = new Date(ts); // WRONG: treated as milliseconds
Результатом будет дата в 1970 году, потому что 1,704,067,200 миллисекунд — это лишь около 19 дней после эпохи.
Когда вы не уверены в единице, добавьте простые проверки:
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());
Если количество «цифр» примерно 10 — вероятно секуды. Если ~13 — вероятно миллисекунды. Также во время отладки выводите toISOString(): это однозначно и помогает быстро заметить ошибки с единицами.
Date в JavaScript может хранить один момент времени, но отображать его в разных часовых поясах.
Внутри Date — по сути «миллисекунды с эпохи» (1970-01-01T00:00:00Z). Это число представляет момент в UTC. Сдвиг появляется, когда вы просите JavaScript отформатировать этот момент как локальное время (по настройкам компьютера/сервера) или как UTC.
Многие API Date имеют локальные и UTC-варианты. Они возвращают разные числа для одного и того же момента:
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)
Если ваша машина в Нью-Йорке (UTC-5), это UTC-время может выглядеть как «19:30» предыдущего дня локально. На сервере, настроенном на UTC, оно будет «00:30». Один и тот же момент, разное отображение.
Логи часто используют Date#toString() или неявную интерполяцию Date, что использует локальный часовой пояс окружения. Это означает, что один и тот же код может печатать разные метки времени на вашем ноутбуке, в CI и в продакшене.
Храните и передавайте время в UTC (например, epoch milliseconds или ISO 8601 с Z). Конвертируйте в локаль пользователя только при отображении:
toISOString() или передавайте миллисекунды эпохиIntl.DateTimeFormatЕсли вы быстро собираете приложение (например, с workflow в Koder.ai), полезно задать это в соглашениях API с самого начала: ясно именуйте поля (createdAtMs, createdAtIso) и держите сервер (Go + PostgreSQL) и клиент (React) в согласии по тому, что означает каждое поле.
Если нужно передавать даты/время между браузером, сервером и базой данных, ISO 8601 строки — самое безопасное по умолчанию. Они явные, широко поддерживаются и (важно) содержат информацию о часовом поясе.
Два хороших формата для обмена:
2025-03-04T12:30:00Z\n- Локальное время со смещением (когда важен локальный показ): 2025-03-04T12:30:00+02:00Что значит Z?\n
Z обозначает Zulu time, другое имя для UTC. То есть 2025-03-04T12:30:00Z — это «12:30 по UTC».
Когда смещения вроде +02:00 важны?\n
Смещения критичны, когда событие привязано к локальному контексту (встречи, бронирования, часы работы). 2025-03-04T12:30:00+02:00 описывает момент, который на два часа опережает UTC, и он не равен 2025-03-04T12:30:00Z.
Строки вроде 03/04/2025 — ловушка: это 4 марта или 3 апреля? Разные пользователи и окружения интерпретируют по-разному. Предпочитайте 2025-03-04 (ISO дата) или полный ISO datetime.
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
Такое «круговое» поведение именно то, что нужно для API: согласованно, предсказуемо и с учётом часового пояса.
Date.parse() кажется удобным: даёшь строку — получаешь timestamp. Проблема в том, что для всего, что не является однозначным ISO 8601, парсинг опирается на эвристики браузера. Эти эвристики различались между движками и версиями, а значит одна и та же строка может распарситься по-разному (или не распарситься вовсе) в разных окружениях.
Date.parse() может давать разный результатJavaScript стандартизирует парсинг надёжно только для строк в стиле ISO 8601 (и даже там детали вроде часового пояса важны). Для «удобных» форматов — типа "03/04/2025", "March 4, 2025" или "2025-3-4" — браузеры могут:
Если вы не можете предсказать точную форму строки, вы не можете предсказать результат.
YYYY-MM-DDРаспространённая ловушка — простая дата "YYYY-MM-DD" (например, "2025-01-15"). Многие разработчики ожидают, что это будет интерпретировано как локальное полуночное время. На практике некоторые окружения трактуют эту форму как UTC полуночь.
Эта разница важна: UTC-полуночь, преобразованная в локальное время, может стать предыдущим днём в отрицательных часовых поясах (например, в Америке) или неожиданно сместить время на часы. Это простой способ получить «почему моя дата сместилась на день?» баг.
Для входа в API/сервер:
2025-01-15T13:45:00Z или 2025-01-15T13:45:00+02:00.\n- Обрабатывайте значения только с датой как данные, а не как момент времени. Если это день рождения или срок, храните как простую строку ("YYYY-MM-DD") и не конвертируйте в Date, если вы не определили ожидаемый часовой пояс.Для ввода от пользователя:
03/04/2025, если интерфейс не жестко задаёт смысл.\n- Предпочитайте контролируемые элементы (date pickers), которые выдают известный формат.\n- Если нужно парсить свободный текст, заранее определите и принудительно принимайте конкретные форматы.Вместо того чтобы полагаться на Date.parse() и его «разберётся», выберите один из подходов:
new Date(year, monthIndex, day) для локальных дат).\n- Хранить и передавать метки времени (миллисекунды эпохи) для точных моментов и форматировать при отображении.Когда данные о времени критичны, «на моём компьютере парсится» недостаточно — сделайте правила парсинга явными и согласованными.
Если ваша цель — «показать дату/время так, как ожидают люди», лучший инструмент в JavaScript — Intl.DateTimeFormat. Он использует правила локали пользователя (порядок, разделители, названия месяцев) и избавляет от хрупкого склеивания строк вроде month + '/' + day.
Ручное форматирование часто жёстко задаёт американский формат, забывает ведущие нули или даёт путаницу 24/12-часового формата. Intl.DateTimeFormat также явно показывает, в каком часовом поясе вы отображаете — критично, когда данные хранятся в UTC, а UI должен отражать локаль пользователя.
Для «красивого» форматирования достаточно dateStyle и timeStyle:
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));
Если нужна постоянная 12/24-часовая система, используйте hour12:
console.log(new Intl.DateTimeFormat('en-US', {
hour: 'numeric',
minute: '2-digit',
hour12: true
}).format(d));
Выберите одну функцию форматирования для каждого «типа» метки времени в интерфейсе (время сообщения, запись лога, начало события) и делайте решение о timeZone намеренным:
Так вы получите последовательный, дружественный к локали вывод без поддержки хрупкого набора собственных формат-строк.
Летнее/зимнее время (DST) — это когда в часовом поясе меняется смещение UTC (обычно на один час) в определённые даты. Сложность в том, что DST не только «меняет смещение» — оно меняет саму существовательность локальных времён.
Когда часы переводят вперёд (spring forward), диапазон локальных времён просто не случается. Например, в некоторых регионах часы прыгают с 01:59 на 03:00, поэтому 02:30 локального времени «не существует».
Когда часы переводят назад (fall back), диапазон локальных времён случается дважды. Например, 01:30 может произойти один раз до перевода и один раз после — то же локальное время может соответствовать двум разным моментам.
Эти операции не эквивалентны вокруг переходов DST:
Если сегодня начинается DST, «завтра в 9:00» может быть всего через 23 часа. Если сегодня заканчивается DST, это может быть 25 часов.
// 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 может удивлятьЕсли вы делаете date.setHours(2, 30, 0, 0) в день «spring forward», JavaScript может нормализовать его в другое валидное время (часто 03:30), потому что 02:30 локально не существует.
setDate), а не простое добавление миллисекунд.\n- При обмене временем через API предпочитайте ISO 8601 с оффсетом или Z, чтобы момент был однозначен.Частая ошибка — использовать Date для представления того, что не является календарным моментом.
Метка времени отвечает на вопрос «когда это произошло?» (конкретный момент вроде 2025-12-23T10:00:00Z). Длительность отвечает на вопрос «как долго?» (например, «3 минуты 12 секунд»). Это разные концепции, и смешение их приводит к путанной арифметике и неожиданным эффектам с часовыми поясами/DST.
Date — не тот инструмент для длительностейDate всегда представляет точку на временной шкале относительно эпохи. Если вы храните «90 секунд» как Date, вы фактически храните «1970-01-01 плюс 90 секунд» в некотором часовом поясе. Форматирование может внезапно дать 01:01:30, сместиться на час или показать дату, которой вы не ожидали.
Для длительностей предпочитайте обычные числа:
HH:mm:ssПростой форматтер, подходящий для таймеров и длительностей медиа:
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)
Если конвертируете из минут — сначала умножьте (minutes * 60) и держите значение числовым до момента рендеринга.
При сравнении времени в JavaScript безопаснее сравнивать числа, а не отформатированный текст. Date объект по сути оборачивает числовую метку (миллисекунды), поэтому сравнения должны сводиться к «число против числа».
Используйте getTime() (или Date.valueOf()) для надёжного сравнения:
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()
}
Избегайте сравнения форматированных строк вроде "1/10/2025, 12:00 PM" — они зависят от локали и не отсортируют корректно. Главное исключение — ISO 8601 строки в одном формате и часовом поясе (например, все ...Z), которые сравниваются лексикографически.
Сортировка по времени проста, если сортировать по миллисекундам эпохи:
items.sort((x, y) => new Date(x.createdAt).getTime() - new Date(y.createdAt).getTime());
Фильтрация внутри диапазона — та же идея:
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;
});
«Начало дня» зависит от того, имеете ли вы в виду локальное время или 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));
Выберите определение рано и придерживайтесь его везде при сравнении и логике диапазонов.
Ошибки с временем кажутся случайными, пока вы не определите что у вас есть (метка? строка? Date?) и где появляется сдвиг (парсинг, конвертация часового пояса, форматирование).
Начните с логирования одного и того же значения тремя разными способами. Это быстро покажет, секуды vs миллисекунды, локальное vs UTC или парсинг.
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());
На что смотреть:
toISOString() сильно не тот год (например, 1970 или далёкое будущее) — подозревайте секунды vs миллисекунды.\n- Если toISOString() выглядит верно, но toString() «сдвинут» — вы видите проблему отображения в локальном часовом поясе.\n- Если getTimezoneOffset() меняется в зависимости от даты — вы пересекаете летнее время.Многие «работает у меня» сообщения — просто разные настройки окружения.
console.log(Intl.DateTimeFormat().resolvedOptions());
console.log('TZ:', process.env.TZ);
console.log(Intl.DateTimeFormat().resolvedOptions().timeZone);
Если ваш сервер в UTC, а ноутбук в локальном часовом поясе, форматированный вывод будет отличаться, если вы явно не укажете timeZone.
Напишите юнит-тесты вокруг границ DST и «крайних» времён:
23:30 → 00:30\n- Несколько часовых поясов, если продукт их поддерживаетЕсли вы быстро итеративно разрабатываете, сделайте эти тесты частью начального набора. Например, при генерации React + Go приложения в Koder.ai можно сразу добавить «контракт времени» (пример нагрузок API + проверки парсинга/форматирования), чтобы регрессии ловились до деплоя.
"2025-03-02 10:00".\n- Форматирование всегда указывает locale и (при необходимости) timeZone.\n- Тесты покрывают границы DST для целевых регионов.Надёжная работа со временем в JavaScript в основном сводится к выбору «источника истины» и последовательности от хранения до отображения.
Храните и вычисляйте в UTC. Локальное время пользователя рассматривайте как деталь представления.
Передавайте даты между системами как ISO 8601 строки с явным оффсетом (предпочтительно Z). Если нужно отправлять числовые эпохи, документируйте единицы и соблюдайте консистентность (миллисекунды — обычный дефолт в JS).
Форматируйте для людей с помощью Intl.DateTimeFormat (или toLocaleString) и указывайте timeZone, когда нужен детерминированный вывод (например, всегда показывать в UTC или в определённом рабочем регионе).
Z (например, 2025-12-23T10:15:00Z). Если используете эпохи, дайте полю явное имя вроде createdAtMs, чтобы единицы были понятны.\n- UI: берите сохранённый UTC-момент и форматируйте его под локаль пользователя. Для интерфейсов планирования явно указывайте часовой пояс и делайте конвертации явными.Подумайте о специализированной библиотеке, если вам нужны повторяющиеся события, сложные правила часовых поясов, DST-устойчивая арифметика («тот же локальный час завтра») или много парсинга несогласованных входов. Ценность в более ясных API и меньшем количестве пограничных багов.
Если хотите углубиться, посмотрите другие руководства по времени на /blog. Если оцениваете инструменты или варианты поддержки, см. /pricing.