Новый Intl.DurationFormat привел к неожиданной ошибке приложения
- вторник, 2 июня 2026 г. в 00:00:14
Если вы уже используете новый 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» вместо формы оплаты.
Два изменения:
Добавить % 24 для часов в getDiff
Передать 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) вместо ошибки.
Поскольку таймер — вспомогательный элемент, он показывает сколько транзакция будет открыта, но не блокирует оплату. Если обернуть его в предохранитель, падение таймера не будет блокировать работу платежной формы.
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 подходит только для элементов, без которых оплата всё равно возможна.
Полифилл formatjs/intl-durationformat@0.7.4 отдает RangeError, если передать в негоhours ≥ 24 без days. Нет предупреждений — сразу исключение.
ErrorBoundary на уровне компонента, не страницы. Оборачивайте некритичные элементы - и всегда с логированием в мониторинг.