Даты в JavaScript: типичные ошибки и как их избежать
new Date('2024-03-15') и new Date('2024-03-15 00:00') дают разное время. Разбираем, почему нативные даты в JS подводят и что с этим делать в 2024 году.

Вы пишете new Date('2024-03-15'), а в консоли получаете Fri Mar 15 2024 03:00:00 GMT+0300 — вместо ожидаемой полуночи 15 марта. Меняете формат на new Date('2024-03-15 00:00') — и дата уезжает на день назад у пользователей в другом часовом поясе. Объект Date в JavaScript устроен так, что одна и та же строка парсится по-разному в зависимости от пробела, буквы T и наличия таймзоны. Ниже — приёмы, которые убирают эти сюрпризы, и цена каждого из них.
Ошибка 1. Парсинг строк с неявной таймзоной
Спецификация ES2015 закрепила ровно один формат, который Date обязан разбирать одинаково во всех движках, — упрощённый ISO 8601. И даже внутри него поведение раздваивается:
'2024-03-15'(только дата) — интерпретируется как UTC, полночь по Гринвичу;'2024-03-15T00:00:00'(дата и время без смещения) — как локальное время браузера;'2024-03-15T00:00:00Z'или+03:00— с явным смещением, однозначно.
А строка '2024-03-15 00:00' (с пробелом вместо T) вообще не входит в стандарт. Её разбор отдан на откуп движку: V8 и SpiderMonkey её проглотят как локальное время, но это не гарантия.
Правило, которое экономит часы отладки: если строку составляете вы, всегда пишите её в формате ISO 8601 с явнымZили смещением. Если строку присылает бэкенд — договоритесь, что там всегда UTC сZ.
// Опасно: результат зависит от таймзоны машины
const a = new Date('2024-03-15'); // UTC-полночь
const b = new Date('2024-03-15T00:00'); // локальная полночь
// В Москве a и b отличаются на 3 часа
// Однозначно:
const utc = new Date('2024-03-15T00:00:00Z');
Ошибка 2. getMonth() возвращает не тот месяц
Месяцы в Date нумеруются с нуля: январь — 0, декабрь — 11. А дни месяца — с единицы. Дни недели через getDay() — снова с нуля, где 0 это воскресенье. Эта непоследовательность даёт классический баг «на месяц вперёд».
const d = new Date(2024, 2, 15); // это 15 МАРТА, а не февраля
console.log(d.getMonth()); // 2, тоже март
// Частая ошибка при сборке даты из полей формы:
const month = 3; // пользователь выбрал "март"
new Date(2024, month, 1); // 1 апреля, а не марта
Отдельный подводный камень конструктора new Date(year, month, day) — он молча нормализует выход за границы. new Date(2024, 1, 30) вернёт 1 марта, а не выбросит ошибку. Иногда это удобно (сдвиги дат), но чаще маскирует опечатку.
Ошибка 3. Мутирующие методы setX
Методы вроде setDate, setMonth, setHours меняют сам объект и возвращают не новую дату, а число — timestamp. Если вы держите дату в переменной и где-то её мутируете, отладка превращается в поиск того, кто испортил объект.
const start = new Date('2024-03-15T00:00:00Z');
const end = start; // это ссылка на тот же объект
end.setDate(end.getDate() + 7);
console.log(start.toISOString()); // 2024-03-22 — start тоже сдвинулся
Лечится либо копированием (new Date(start)), либо переходом на неизменяемые API — об этом ниже.
Что использовать вместо голого Date
Долгие годы стандартом де-факто была библиотека Moment.js, но её команда официально перевела проект в статус legacy и не рекомендует для новых приложений — из-за мутабельности и большого размера бандла. Сегодня выбор такой:
| Вариант | Когда брать | Цена |
|---|---|---|
| Temporal (нативный) | Новые проекты, где допустим полифил | Ещё Stage 3, в браузерах по умолчанию не везде — нужен полифил |
| date-fns | Нужны функции над нативным Date, tree-shaking | Работаете всё ещё с Date со всеми его особенностями |
| Luxon | Активная работа с таймзонами и локалями | Отдельный класс DateTime, свой API |
| Голый Date + Intl | Простое форматирование без зависимостей | Ручная возня с таймзонами |
Temporal — будущий встроенный API, который решает почти все перечисленные проблемы: неизменяемые объекты, явное разделение «даты», «времени» и «момента», отдельный тип для зоны. На момент написания предложение находится на Stage 3 в TC39, а движки внедряют его постепенно. Проверяйте поддержку в вашей среде и подключайте полифил @js-temporal/polyfill, пока встроенной реализации нет.
Форматирование без библиотек: Intl
Intl.DateTimeFormat встроен во все актуальные браузеры и Node.js и умеет форматировать с учётом локали и таймзоны — без ручной сборки строк.
const d = new Date('2024-03-15T12:30:00Z');
// Формат под русскую локаль в конкретной зоне
const fmt = new Intl.DateTimeFormat('ru-RU', {
dateStyle: 'long',
timeStyle: 'short',
timeZone: 'Europe/Moscow',
});
console.log(fmt.format(d)); // 15 марта 2024 г., 15:30
Ключевой параметр здесь — timeZone с названием зоны из базы IANA (Europe/Moscow, America/New_York). Он избавляет от привязки к часовому поясу машины, где выполняется код.
Ошибка 4. Сравнение дат через == и ===
Два объекта Date, указывающие на один момент, не равны через === — это разные ссылки. Сравнивать нужно числовые представления.
const x = new Date('2024-03-15T00:00:00Z');
const y = new Date('2024-03-15T00:00:00Z');
console.log(x === y); // false — разные объекты
console.log(x.getTime() === y.getTime()); // true
console.log(+x === +y); // true — унарный плюс приводит к числу
Для «больше/меньше» операторы < и > работают напрямую, потому что при сравнении объект приводится к числу. А вот == между датами и === — нет.
Ошибка 5. Игнорирование Invalid Date
Некорректная строка не бросает исключение. new Date('не дата') вернёт объект, но при попытке взять из него значение получите NaN. Проверяйте валидность явно:
function isValidDate(d) {
return d instanceof Date && !Number.isNaN(d.getTime());
}
const parsed = new Date(userInput);
if (!isValidDate(parsed)) {
// обработать некорректный ввод
}
Если что-то всё равно ломается
- Дата уехала на день у части пользователей — почти всегда парсинг строки без таймзоны трактуется как локальное время. Добавьте
Zили смещение в исходную строку. - Месяц на единицу больше/меньше — забыли про нумерацию с нуля в конструкторе и
getMonth. - Дата меняется «сама» в неожиданном месте — где-то вызывается мутирующий
setXна общей ссылке. Ищите по коду.setи копируйте объект перед изменением. - Разное поведение на бэкенде и в браузере — Node.js берёт таймзону из окружения (
TZ), браузер — из настроек ОС. Фиксируйте зону черезtimeZoneвIntl.
Что делать дальше
Первый шаг — договоритесь на уровне проекта об одном правиле: все даты между фронтом и бэком передаются в ISO 8601 с Z, а к локальному виду приводятся только при отображении через Intl. Второй — посмотрите proposal Temporal в репозитории TC39 и оцените, готовы ли вы подключить полифил: для нового кода он снимает большинство описанных граблей разом. Работу с диапазонами дат, «сколько дней между» и рекуррентные события здесь не разбирали — это отдельная тема, где ручная арифметика над timestamp особенно опасна из-за переходов на летнее время.
Тест и проверочная: Быстрые ответы с объяснением, почему остальные варианты неверны
Спросить за 10 ₽Источники: Date — JavaScript | MDN Web Docs, TC39 proposal-temporal
Частые вопросы
А если у меня разное поведение в Node.js и в браузере?
Стоит ли ещё использовать Moment.js в новых проектах?
Temporal уже можно использовать в проде?
Почему new Date('2024-03-15') даёт полночь по UTC, а не по локальному времени?
Почему сравнение двух дат через === возвращает false, хотя даты одинаковые?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.