惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

腾讯CDC
IT之家
IT之家
有赞技术团队
有赞技术团队
WordPress大学
WordPress大学
Apple Machine Learning Research
Apple Machine Learning Research
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
人人都是产品经理
人人都是产品经理
The Cloudflare Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
博客园 - 【当耐特】
V
V2EX
Last Week in AI
Last Week in AI
H
Help Net Security
The GitHub Blog
The GitHub Blog
S
SegmentFault 最新的问题
F
Fortinet All Blogs
I
InfoQ
宝玉的分享
宝玉的分享
A
About on SuperTechFans
MongoDB | Blog
MongoDB | Blog
Microsoft Azure Blog
Microsoft Azure Blog
Blog — PlanetScale
Blog — PlanetScale
B
Blog

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания
Новый Intl.DurationFormat привел к неожиданной ошибке при...
VolhaIvanova · 2026-06-01 · via Все публикации подряд на Хабре

Если вы уже используете новый Intl.DurationFormat в совсем проекте, то вам будет полезен мой кейс и поможет вам сэкономить пару часов на дебагинг.

Продукт над которым я работаю - это платежная форма. Это стабильное давно работающее приложение и вдруг пользователи стали сообщать об «Unknown error» при попытке провести транзакцию. При этом проблема была только для одного вида транзакций — «Счет».

Сначала проверили бэкенд, там все запросы проходят корректно, я стала проверять фронт и к моему удивлению никаких особенностей для этого типа транзакций добавлено не было и в кодовой базе она ничем не отличалась от обработки других транзакций.

Если коротко, платежная форма состоит из инпутов, таймера обратного отсчета и кнопок. Соответственно, подозрения пали на работу таймера в первую очередь. После сравнения данных транзакции для успешных и выдающих ошибку, паттерн обнаружился в данных бэкенда:

  {
    "transaction": {
      "id": 111111111,
      "status": "Pending",
      "createdAt": "2026-05-11T08:39:15+00:00",
      "expiresAt": "2026-07-01T08:39:15+00:00"
    }
  }

expiresAt — июль, а сейчас май. Так, срок жизни транзакции типа «Счет» ~55 дней, в то время как все другие транзакции живут не больше 15 минут. Дело в том, что банковские переводы ждут подтверждения от банка и могут висеть несколько дней. Именно поэтому баг проявлялся только для этого типа — остальные работали корректно.

У нас в проекте для таймера обратного отсчёта используется компонент TextTimer с полифиллом formatjs/intl-durationformat@ 0.7.4. Полифилл подключён в корне приложения глобально, поэтому нативная реализация браузера не задействована.

Код до фикса:

 const formatter = new Intl.DurationFormat(i18n.locale, {
   hoursDisplay: "auto",
   secondsDisplay: "always",
   style: formatStyle,
 });
 
 function getDiff(unit: dayjs.OpUnitType) {
   let diff = expiresAtDayjs.diff(now, unit);

   if (unit === "m" || unit === "s") {
     diff %= 60;
   }
 
   return Math.max(0, diff);
 }

 formatter.format({
   hours: getDiff("h"),    // = 1320
   minutes: getDiff("m"),
   seconds: getDiff("s"),
 }); // 💥 RangeError

Так в чем же проблема?

Дело в том, что если в таймер приходит длительность больше суток, а в моем случае это было 55 дней, getDiff(“h”) возвращает 1320 — потому что для часов не было % 24, и days не передавался в .format().

Полифилл видел 1320 часов, Intl.DurationFormat не понимал как обработать такой период и выбрасывал RangeError. Ошибка всплывала наверх без ErrorBoundary на компоненте, отсюда «Unknown error» вместо формы оплаты.

Решение

Два изменения:

  1. Добавить % 24 для часов в getDiff

  2. Передать days в .format()

const formatter = new Intl.DurationFormat(i18n.locale, {
  hoursDisplay: "auto",
  secondsDisplay: "always",
  style: formatStyle,
});

function getDiff(unit: dayjs.OpUnitType) {
  let diff = expiresAtDayjs.diff(now, unit);

  if (unit === "h") {
    diff %= 24;          // ← добавили
  } else if (unit === "m" || unit === "s") {
    diff %= 60;
  }

  return Math.max(0, diff);
}

formatter.format({
  days: getDiff("d"),    // ← добавили
  hours: getDiff("h"),   // теперь 1320 % 24 = 8 ✓
  minutes: getDiff("m"),
  seconds: getDiff("s"),
});

Теперь пользователь видит 55 дн. 10:05:00 (RU) или 55d 10:05:00 (EN) вместо ошибки.

А что если бы был ErrorBoundary?

Поскольку таймер — вспомогательный элемент, он показывает сколько транзакция будет открыта, но не блокирует оплату. Если обернуть его в предохранитель, падение таймера не будет блокировать работу платежной формы.

 class TimerErrorBoundary extends React.Component {
   componentDidCatch(error: Error) {
     logger.error('TextTimer crashed', { error }); // обязательно!
   }

   render() {
     if (this.state.hasError) return null;
     return this.props.children;
   }
 }

Главное — внутри ErrorBoundary всегда нужно логировать ошибку, иначе баг останется невидимым на проде. Для логирования подойдёт любой мониторинг ошибок, например, Sentry, Datadog, или собственный logger если он есть в проекте.

Также стоит понимать, что обернуть в ErrorBoundary для того, чтобы ошибка не блокировала весь компонент можно только второстепенные компоненты. То, что отвечает за вывод суммы, реквизиты и кнопка оплаты должны падать громко. ErrorBoundary подходит только для элементов, без которых оплата всё равно возможна.

Выводы

  1. Полифилл formatjs/intl-durationformat@0.7.4 отдает RangeError, если передать в негоhours ≥ 24 без days. Нет предупреждений — сразу исключение.

  2. ErrorBoundary на уровне компонента, не страницы. Оборачивайте некритичные элементы - и всегда с логированием в мониторинг.