Обработка ошибок в JavaScript
Привет, Хабр!
Тема обработки ошибок в JavaScript возникает не только у каждого новичка, но и матерого разработчика. Замечу, что тема уже довольно заезжена, поэтому я позволю себе резюмировать в кратком изложении все, что действительно эффективно и проверено в бою мною, коллегами и гуру IT.
Итак, для того, чтобы вам было понятно, в чем суть и есть ли что-то новое для вас, ниже представлена структура заметки:
Ошибка в JavaScript?
Железобетонные методы обработки ошибок
Облегчаем себе жизнь
Ошибки зависимостей
Ошибка в JavaScript?
Не погружаясь в этимологию ошибки в JavaScript, охарактеризуем ее абстрактно, поскольку сам по себе объект ошибки в JS не стандартизирован полностью.
Ошибка в JS — это «выбрасывание» исключения (throw of an exception). Исключение должно быть обработано программой, в противном случае интерпретатор вернет нас на то место, где это исключение было выброшено. По умолчанию исключение выбрасывает объект Error.
Неважно, пишете ли вы Frontend или Backend, подход к обработке один — поймать злосчастное исключение и обработать. Обрабатывать нужно все, особенно в проде.
Сразу просветим пару нестандартных ситуаций (кому как):
ошибка извне программы,
терминальная ошибка.
Терминальная ошибка — это код ошибки, который возвращает ОС или демон.
Ошибка извне программы может быть частным случаем терминальной, но тем не менее она должна быть обработана.
Любая из этих нестандартных ситуаций может попасть в общий стек ошибок и будет обработана, т.к. каждое выброшенное исключение, попавшее в программу, захватывает стек.
Самый главный вопрос — когда возникает ошибка?
Ошибка возникает в том случае, когда программа или интерпретатор не может перейти к следующей инструкции по некоторым причинам:
Синтаксическая ошибка (забыли запятую, скобку и т.д.)
Ошибка интерпретатора (обращение к несуществующей переменной и т.д.)
Ошибка исполнения (тип переменной оказался, например, undefined) — самая частая в работающем приложении
И еще несколько вариантов, с которыми вы можете ознакомиться тут.
В каждом из случаев есть человеческий фактор, чтобы этого не допускать используйте линтеры, которые следят за чистотой вашего кода и минимизируют риск возникновения ошибок еще до запуска. Вдобавок к этому, ошибки исполнения — частое явление в JavaScript, следить за этим помогает Typescript.
Железобетонные методы обработки ошибок
Чтобы сражаться с врагом, нужно знать его в лицо, поэтому ниже основные свойства объекта Error:
name — название ошибки;
message — текст выбрасываемой ошибки;
stack — стек вызовов, приведших к ошибке.
Важно заметить, что свойства не стандартизированы, а также помним, что исключение может быть любым типом данных.
Из этого набора информации при обработке ошибок самым важным является их классификация. По моему мнению, если удалось правильно классифицировать выброшенное исключение — это 80% работы. Остальные 20% завязаны на правильной обработке, ведь каждое приложение — это бизнес, следовательно, минимизация ошибок в бизнесе — прирост конверсии.
В зависимости от приложения классификация и обработка могут быть написаны собственноручно, либо можно задействовать готовые инструменты. Мы не будем рассматривать производные методы отлова ошибок, такие как TDD или E2E, а ограничимся только девелоперскими инструментами, но прежде определим, что мы желаем получить от инструмента:
стек вызовов, приведших к ошибке;
уровень ошибки (фатальная, критическая, баг, неожиданная и т.д.);
класс ошибки (сетевая, сервисная, пользовательская и т.д.);
хранение ошибки для анализа и пост-обработки;
логирование;
профилирование / метрика.
Из всего обилия существующих инструментов выбирать нужно именно по этим критериям. Разница инструментов, как правило, в том, что одни заточены под разработку приложения, а другие под работу приложения в релизе. Суть одна, но какому-то из критериев просто уделяется больше внимания.
Уметь выбирать библиотеку — отличный навык, а умение компоновать поможет вам приблизиться к идеалу. Предлагаю разобрать способы именно для процесса разработки.
Придерживаясь методологий SOLID и DRY, нам следует внедрить наш обработчик (middleware) на самый верхний уровень и уже оттуда обрабатывать все ошибки, которые прошли мимо. Middleware может быть как написанный самостоятельно, так и из библиотеки. Ниже примеры.
Для Node.js
Для Vanilla JS
Для React
Для Angular
Для Vue
Данные примеры касаются верхнеуровнего отлова и обработки ошибок, но возникает резонный вопрос: как быть с частными случаями, встречающимися в парадигме JavaScript? Ниже несколько примеров.
Всегда оборачивайте асинхронный код в try…catch, а также вызовы сторонних библиотек.
Вот пример:
// ...
const middlewareRequest = async (req) => {
try {
const { data } = await axios.get(req);
return data;
} catch (err) {
throw new Error(err);
}
}
// ...
Опытный архитектор может заметить, что если оборачивать все асинхронные конструкции в try…catch, то это сродни «аду коллбэков», поэтому придерживайтесь методологии DRY и пишите все на верхнем уровне, если позволяет ваша архитектура.
То же касается и работы с событийной моделью: можно назначать middleware через Функции Высшего Порядка — в будущем это позволит вам быстро масштабироваться.
Пример обработки глобального события:
// ...
const wrapEventWithExcpetionHandler = (middleware) => (e) => {
const { error } = e; // предположим, что ошибка в этом поле
if (error) {
throw new Error(error);
}
try {
return middleware(e);
} catch (err) {
throw new Error(err);
}
}
window.addEventListener('mousemove', wrapEventWithExceptionHandler(middlewareGlobalMouseMove));
// ...
Как видно из примеров выше, следуя путем самостоятельной классификации, разработчику придется сильно напрячься и писать велосипед для каждой библиотеки или фреймворка, следовательно, эффективнее выбирать готовый инструмент в зависимости от потребностей.
В любом случае эти примеры могут быть вам полезны, поскольку внедрение даже готового инструмента подразумевает «поднятие» обработчиков на верхний уровень.
Еще раз взглянув на примеры, можно удостовериться в одном: любая пойманная ошибка в идеале должна иметь одинаковый набор параметров информативности независимо от инструмента и среды исполнения, а ведь мы еще не рассмотрели логирование, профилирование и хранение ошибок на сервере. Соответственно, из этого можно сделать вывод, что слой обработки ошибок должен сосуществовать со слоем исполнения, а это уже влечет за собой последствия и накладывается на архитектуру.
Так для чего же нужны эти приемы?
Ответ — для ведения простой и надежной разработки. Ваша задача как разработчика — делать отказоустойчивый код и при возникновении ошибки не дебажить все подряд, а сразу бить в «яблочко» и устранять проблему. Это попросту экономит ваше время, силы и деньги бизнеса.
Работайте с DevTools и выбрасывайте исключения, другие разработчики будут вам благодарны, опираясь на этот гайд. Обязательно ознакомьтесь, если не знали, вот пример:
// ...
/* обычный console.log может превратиться в нечто большее */
/*
как правило, начинающие программисты логируют по одной переменной,
мы же можем форматировать строки с любым количеством аргументов
*/
console.log('Check:\r\n username - %s\r\n age - %i\r\n data - %o', 'Mike', 23, {status: 'registered'});
/*
Check:
username - Mike
age - 23
data - {status: "registered"}
*/
/* выводить таблицы массивов */
console.table([{username: 'Mike', age: 23}, {username: 'Sarah', age: 46}]);
/* или просто логировать данные в их первоначальном виде */
console.dir(document.body.childNodes[1]);
// ...
Далее рассмотрим инструменты, которые собирают данные не только от компонентов, но и от сервисов (например, сетевые запросы, запросы к устройству и т.д.), а также сторонних библиотек и приложений, что кратно улучшает вашу производительность при обработке ошибок.
Облегчаем себе жизнь
1. Рекомендую взять за правило: перед началом каждой разработки централизовать любое логирование, особенно ошибок. С этой задачей помогут справиться библиотеки по типу log4js. Это сразу даст вам понять, ошибка в вашем приложении, либо извне.
2. Используйте Брейкпоинты в DevTools! Это важно уметь делать. Это как машина времени программы, вы останавливаете интерпретатор на нужной строчке и вам даже не нужна консоль — просто смотрите значения переменных и поймете, что не так. Делается это простым кликом на нужной строчке во вкладке Source. Выбираете нужный файл, ставите брейкпоинт и перезапускаете программу. Для удаления брейкпоинта кликните на ту же строчку.
3. Стараемся перехватывать все ошибки и исключения на верхнем уровне.
4. Хранение ошибок на сервере больше относится к проду, но имеем в виду, что готовый инструмент прекрасно справляется с данной задачей (см. ниже).
5. Профилирование — тема тоже непростая, если вы знаете, что это измерение времени от начала до конца исполнения монады, вы уже на полпути. К счастью, DevTools позволяют делать замеры без вмешательства в код.
Эти правила априори необходимы, даже если вы опытные разработчик, вам всегда нужно знать, как ведет себя программа в конкретный момент времени.
Для ПРОДвинутых
Если вы уже как рыба в воде при работе с ошибками в JS, рекомендую посмотреть на сервисы для автоматизации сбора и ведению статистики ошибок.
Такие сервисы, как Sentry и Rollbar, уже заточены под работу со многими популярными пакетами и требуют от вас только установки, задания конфигурации в точке входа вашего приложения и определение надстроек в зависимости от вашей архитектуры.
Их преимущество в том, что они покрывают большую часть потребностей, о которых мы писали выше и требуют вашего минимального вмешательства. На выходе вы получите приятные бонусы с красивым интерфейсом и все пункты, которые мы указали важными (Железобетонные методы обработки ошибок).
Также рекомендую ознакомиться на досуге с Graphana, это за рамками статьи, т.к. относится не только к JavaScript, но очень хорошо коррелирует с нашей темой и позволяет отображать на графиках текущее состояние приложений, слать уведомления в чат об ошибках и не только.
Ошибки зависимостей
Очень частым явлением среди разработчиков является работа со сторонними пакетами, в которых тоже могут встречаться ошибки.
Тут нет выработанного универсального решения для отлова или же игнорирования, т.к. многое зависит непосредственно от сборки самого пакета. Какие советы тут можно дать? Их немного:
Самым важным в логировании исключений являются уровни ошибок. Вы можете задавать их посредством встроенного console (log, warn, error, info), либо в сторонних библиотеках (см. выше log4js). Здесь решением проблемы является максимальное разделение ошибок вашего приложения и стороннего, но не переборщите, ведь могут быть действительно важные исключения.
Разделяйте ваши сборки на production/development/test и используйте source-map во время разработки либо пре-релиза, это позволит вам получать более детальную информацию в бою о том, что пошло не так с информативным стеком ошибки.
Другим способом в перехвате ошибок зависимостей является реальное устранение проблемы, например, посредством Pull Request. Для ленивых можно использовать Fork с фиксом, но тогда его нужно поддерживать, а некоторые проекты не всегда позволяют это делать.
Ну, и самым изощренным и неочевидным является использование соответствующих надстроек для babel. Транспайлинг посредством babel работает через AST, который в первом приближении разбирает весь код JavaScript на дерево с вершинами. Есть специальные плагины, которые делают необходимые обертки для удобства разработчиков, по типу полифиллов, перегрузок, а также оборачиванию в специальные конструкции. Оборачивать можно, как вы догадались, и обработку ошибок, но данное решение должно иметь острую необходимость, просто имейте это в виду.
Заключение
Выше были рассмотрены вполне стандартные методы обработки ошибок, а также продемонстрированы примеры техник с кодом для популярных пакетов. Дополнением выступают инструменты по автоматизации сбора и обработки ошибок. Подробную информацию читайте по ссылкам.
Комбинируйте несколько методов и доверяйте проектам с хорошей репутацией. Обязательно логируйте во время разработки, брейкпоинты только помогают понять проблему в конкретном случае, но не являются лекарством.
Автор: Ришат Габайдуллов, Руководитель группы практики Frontend компании «Рексофт».