Новый Intl.DurationFormat привел к неожиданной ошибке приложения

от автора

Если вы уже используете новый 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 на уровне компонента, не страницы. Оборачивайте некритичные элементы — и всегда с логированием в мониторинг.

ссылка на оригинал статьи https://habr.com/ru/articles/1042182/