10 паттернов обработки ошибок в JavaScript, которые действительно важны¶
От try/catch и собственных классов ошибок до React Error Boundaries и запасных обработчиков на уровне процесса.
Обработка ошибок проста, пока код небольшой.
Вызываете функцию, оборачиваете её в try/catch, пишете ошибку в лог и идёте дальше.
Настоящие приложения устроены иначе. Ошибки появляются внутри промисов, таймеров, обработчиков событий, вызовов API, кода отрисовки или на несколько слоёв ниже функции, которая запустила операцию.
Сложность редко в том, чтобы написать catch. Сложность в том, чтобы решить, где ошибке место, кто должен её обработать и что должно произойти дальше.
Это пятая часть серии JS Coding Techniques.
Предыдущие части:
- Часть 1. 10 современных приёмов JavaScript, которые убирают шаблонный код
- Часть 2. 20 современных приёмов JavaScript для более чистого повседневного кода
- Часть 3. 10 паттернов debounce и throttle, которые стоит знать
- Часть 4. 10 советов по асинхронной конкурентности в JavaScript и типичные ловушки
Дальше — десять паттернов обработки ошибок, которые полезны в повседневном JavaScript.
1. Держите обработку асинхронных ошибок в одном месте¶
У цепочек промисов своя обработка ошибок:
В этом подходе нет ничего плохого.
Но если функция уже использует async/await, блок try/catch обычно делает поток управления понятнее.
Это особенно полезно, когда несколько операций относятся к одной задаче.
Один блок теперь представляет одну операцию с точки зрения приложения.
Важная деталь: у отклонённого промиса всё равно должен быть потребитель. Если асинхронная функция отклоняется, а её никто не ожидает с обработкой ошибки и к ней не прикреплён .catch(), отказ становится необработанным.
Поэтому так вызывать рискованно:
Если за сбой отвечает вызывающий код, обрабатывайте его там:
Полезное правило простое: у каждого промиса в итоге должен быть владелец, отвечающий за его сбой.
2. try/catch не перехватывает асинхронные колбэки¶
На первый взгляд это выглядит разумно:
Но блок catch эту ошибку никогда не получит.
Колбэк выполняется позже, когда исходный блок try уже завершился.
Если сам колбэк может выбросить исключение, обрабатывайте ошибку внутри этого пути выполнения:
Та же идея относится ко многим API на колбэках.
Промисы ведут себя иначе, потому что отказ проходит по цепочке промиса:
Работая с асинхронным кодом, всегда задавайте один вопрос: куда уйдёт ошибка из этого колбэка или промиса?
Если ясного ответа нет, скорее всего, путь обработки ошибки отсутствует.
3. Добавьте глобальный предохранитель для необработанных отказов промисов¶
Основную работу должна делать локальная обработка ошибок.
И всё же приложению полезно иметь последнюю страховочную сеть для сбоев, которые ушли от обычных обработчиков.
В браузере:
Можно также наблюдать необработанные синхронные ошибки:
Для Node.js:
Эти обработчики не должны становиться обычной архитектурой приложения.
Их задача — поймать сбои, которые проскочили через всё остальное, и убедиться, что вы о них узнали.
В продакшене часто недостаточно писать только в консоль. Когда приложение работает на чужой машине или сервере, нужен контекст, достаточный, чтобы разобраться, что произошло.
Сюда могут входить стек вызовов, версия приложения, выполняемая операция, идентификатор запроса или другая нечувствительная диагностическая информация.
Считайте глобальные обработчики последней тревогой, а не первой линией обороны.
4. Заведите классы ошибок для сбоев, которые нужно различать¶
Бросать везде Error работает, пока вызывающему коду не нужно реагировать на разные сбои по-разному.
Рассмотрим ошибки валидации и сети.
Теперь функция может сообщить больше, чем текстовое сообщение:
Вызывающий код может отреагировать на фактический тип ошибки:
Обратите внимание на финальный throw error.
Это важно.
Если текущий слой умеет обработать ошибку валидации, но не знает, что делать с неожиданным сбоем базы данных или сети, он не должен молча её поглотить.
Собственные ошибки полезнее всего, когда несут информацию, на которую вызывающий код может отреагировать.
5. Сохраняйте исходную ошибку через cause¶
Иногда низкоуровневой ошибке нужен дополнительный контекст, прежде чем она поднимется по стеку.
Представьте, что запрос к API завершился неудачей:
Сообщение верхнего уровня объясняет, что приложение пыталось сделать:
Но исходная ошибка по-прежнему доступна через error.cause.
Это гораздо лучше, чем заменять исходный сбой новой ошибкой, не связанной с ним.
Без cause такой код теряет полезный контекст:
Хорошей ошибке часто нужны две части информации:
- Какая операция не удалась?
- Почему она не удалась?
Error.cause даёт чистый способ сохранить и то, и другое.
6. Решите, когда сбой исключительный, а когда обычный¶
Не каждая неуспешная операция заслуживает исключения.
Допустим, поиск может законно ничего не вернуть.
Можно написать так:
Но если «не найдено» — ожидаемый результат, возврат null описывает API яснее:
Вызывающий код тогда обрабатывает эту ветку как обычную:
Сравните это с функцией, где сбой действительно не даёт запрошенной операции продолжиться:
Нет универсального правила, что бизнес-сбои должны возвращать значения, а ошибки программирования — бросать исключения.
Лучше спросить иначе: этот результат входит в ожидаемый контракт функции или означает, что операция не удалась?
Выбирайте API исходя из этого различия и держите его согласованным.
7. Не ловите ошибку только для того, чтобы она исчезла¶
Это одна из самых лёгких ошибок, которые можно написать:
Приложение продолжает работу так, будто сохранение удалось.
Позже кто-то сообщает, что настройки всё время пропадают.
Теперь проблем две: исходный сбой и отсутствие следов, которые его объясняют.
Если текущий слой может восстановиться, восстанавливайтесь:
Если восстановиться нельзя, добавьте полезный контекст и пробросьте сбой:
Бывают случаи, когда игнорировать ошибку — намеренное решение.
Например, код очистки может пытаться удалить временный ресурс, которого уже нет.
Это не слепое проглатывание ошибки. Это явная обработка сбоя, который вы уже понимаете.
У блока catch должна быть причина существовать.
8. Используйте Error Boundaries для сбоев отрисовки в React¶
У React-приложений есть ещё одна категория сбоев: компонент может выбросить исключение, пока React рисует интерфейс.
Error Boundary может изолировать этот сбой и заменить сломанную часть дерева запасным интерфейсом.
Затем оберните ту часть интерфейса, которую хотите изолировать:
Если Dashboard или один из его потомков выбросит исключение во время отрисовки, React покажет запасной интерфейс, а не потеряет весь этот раздел.
Но Error Boundary — не универсальный try/catch.
Например, ошибкам из обработчика события всё равно нужна собственная обработка:
Используйте Error Boundaries для тех сбоев, которые они предназначены изолировать, а операционные асинхронные сбои обрабатывайте там, где эти операции происходят.
9. Считайте обработчики процесса Node.js аварийными выходами¶
Сервис на Node.js может слушать ошибки, которые ушли от обычной обработки приложения.
Можно также наблюдать необработанные отказы промисов:
Важно и то, чего туда класть не стоит.
Не пытайтесь продолжать обычную бизнес-логику из uncaughtException.
Необработанное исключение означает, что приложение пришло в состояние, которое обычный поток управления не учитывал. В зависимости от того, что сломалось, внутреннему состоянию уже нельзя доверять.
Обработчики уровня процесса поэтому полезны для финального логирования, отчёта и контролируемого завершения.
В продакшене менеджер процессов, контейнерная платформа или система оркестрации затем может запустить новый процесс.
Обработчики запросов по-прежнему должны разбирать ожидаемые сбои локально.
Хуки уровня процесса — страховочная сеть под этой архитектурой, а не её замена.
10. Используйте опциональную цепочку для отсутствующих данных, а не для сломанных допущений¶
Опциональная цепочка и оператор нулевого слияния могут предотвратить лишние сбои, когда отсутствие данных ожидаемо.
Это гораздо чище, чем проверять каждый уровень вручную:
Но защитный синтаксис может и скрывать баги, если применять его слишком агрессивно.
Рассмотрим такой код:
Отсутствие платежа действительно должно означать $0?
Возможно.
А возможно, у каждого завершённого заказа обязаны быть платёжные данные, и их отсутствие говорит о повреждённых или неполных данных.
В таком случае полезнее упасть явно:
Опциональная цепочка отвечает на вопрос:
Что должно произойти, если этого значения нет?
Обработка ошибок отвечает на другой:
Что должно произойти, если эта операция не может корректно продолжиться?
Они хорошо работают вместе, но решают разные задачи.
Практическая стратегия обработки ошибок¶
Отдельные паттерны важны, но согласованность важнее.
Для большинства приложений полезный подход выглядит примерно так:
| Ситуация | Что делать |
|---|---|
| Ожидаемое отсутствие значения | вернуть null, undefined или Result |
| Восстановимый сбой операции | поймать локально и показать запасной вариант |
| Неожиданный сбой нижнего уровня | добавить контекст и пробросить |
| Сбой отрисовки | Error Boundary |
| Необработанный сбой приложения | глобальный отчёт |
| Невосстановимый сбой процесса Node.js | лог, отчёт, остановка, перезапуск |
try/catch не нужен вокруг каждой функции.
Нужно ясное владение сбоем.
Функция разбора может бросить исключение, потому что из некорректного ввода нельзя получить результат. Функция поиска может вернуть null, потому что пустой результат ожидаем. Обработчик в интерфейсе может поймать сетевую ошибку, потому что знает, как рассказать пользователю, что произошло.
Эти решения образуют контракт обработки ошибок вашего приложения.
Итог¶
Хорошая обработка ошибок почти незаметна, пока всё работает.
Её ценность проявляется, когда что-то ломается.
Вместо пустого экрана пользователь получает запасной интерфейс. Вместо необъяснимого бага в продакшене вы получаете стек вызовов с контекстом. Вместо того чтобы каждая функция изобретала собственное соглашение о сбоях, вызывающий код знает, ждать ли null, отклонённый промис или конкретный тип ошибки.
Цель не в том, чтобы поймать каждую возможную ошибку.
Цель в том, чтобы сбои были предсказуемыми.
Ловите ошибки там, где от них действительно можно восстановиться. Добавляйте контекст, когда восстановиться нельзя. Сохраняйте исходную причину, избегайте молчаливых блоков catch и оставляйте глобальные обработчики последней страховочной сетью.
Так вы получаете кое-что полезнее кода, который просто «не падает»: код, который говорит, что пошло не так, когда это всё-таки случается.
Источник: https://jsdevspace.substack.com/p/10-javascript-error-handling-patterns