В этой статье я рассмотрю вопрос выявления и обработки ошибок, возникающих на фронтенде (браузер или web-view).
На фронтенде код JS выполняется в браузере. JavaScript не является компилируемым языком, поэтому всегда существует вероятность ошибки исполнения при непосредственном использовании программы. Ошибка исполнения блокирует код, расположенный после места ошибки, и пользователи программы рискуют остаться с нефункциональным экраном приложения, который можно будет лишь перезагрузить или закрыть. Но есть методы выявления ошибок и их безопасного сопровождения, позволяющие избежать таких ситуаций.
1. Средства языка JavaScript
Блоки try/catch
Функции, которые могут выполниться с ошибкой, оборачивают в блоки try/catch. Сначала программа пытается выполнить код в блоке try. Если по каким-то причинам выполнение кода сломалось, программа переходит в блок catch, где доступны три параметра:
- name — стандартизированное имя ошибки;
- message — сообщение о деталях ошибки;
- stack — текущий стек вызова, в котором произошла ошибка.
То есть:
try {
callFunc();
} catch (e) {
console.log(e.name) // ReferenceError
console.log(e.message) // callFunc is not defined
console.log(e.stack) // ReferenceError: callFunc is not defined at window.onload
}
Для разработчика главное то, что программа сможет продолжить выполнение после блока catch. Таким образом, взаимодействие с пользователем не прервется.
С помощью блока try/catch также можно вызывать собственные ошибки, например при проверке данных:
const user = {
name : «Mike»
};
try {
if (!user.age) throw new SyntaxError(«User age is absent!»);
} catch (e) {
console.log(e.name) // SyntaxError
console.log(e.message) // User age is absent!
console.log(e.stack) // SyntaxError: User age is absent! at window.onload
}
Наконец, можно расширить эту инструкцию еще одним блоком — finally, который выполняется всегда: и в случае, если в try не было ошибки, и в случае, если управление перешло в блок catch:
try {
callFunc();
} catch (e) {
console.log(e)
} finally {
...
}
Иногда используют инструкцию try/finally (без catch), чтобы была возможность продолжать использование кода без обработки конкретных ошибок.
Событие window.onerror
Часто бывает полезно знать, что скрипт на странице сломался, даже если программа сломалась и пользовательская сессия закончилась неудачно. Обычно эту информацию потом используют в системах логирования ошибок.
У глобального объекта window существует событие onerror (использовать его надо аккуратно: в разных браузерах реализация может отличаться!):
window.onerror = function(message, url, line, col, error) {
console.log(`${message}\n В ${line}:${col} на ${url}`);
};
Если разместить этот код в начале скрипта или подгрузить отдельным скриптом в первую очередь, то при любой ошибке ниже разработчику будет доступна подробная информация о ней.
Однако полная информация доступна только для скриптов, которые были загружены с того же домена. Если сломанный скрипт загружен с другого домена, window.onerror сработает, однако подробностей ошибки не будет.
Компоненты фреймворков
Некоторые JS-фреймворки (React, Vue) предлагают собственные решения по обработке ошибок. Например, React сможет отрисовать специальную верстку на месте блока, в котором произошла ошибка:
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
// Обновить состояние с тем, чтобы следующий рендер показал запасной UI.
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// Можно также сохранить информацию об ошибке в соответствующую службу журнала ошибок
logErrorToMyService(error, errorInfo);
}
render() {
if (this.state.hasError) {
// Можно отрендерить запасной UI произвольного вида
return <h1>Что-то пошло не так.</h1>;
}
return this.props.children;
}
}
<ErrorBoundary>
<MyWidget />
</ErrorBoundary>
Фактически, компонент React оборачивается специальным компонентом, который обрабатывает ошибки. Это напоминает оборачивание функций с помощью конструкции try/catch.
2. Средства сборки проекта
Современные JS-скрипты, как правило, делаются транспилируемыми. То есть разработка ведется с использованием последних стандартов ES. А затем код разработчика с помощью сборщика проектов (такого как Webpack) преобразуется в код, который будет гарантированно работать в выбранном числе браузеров.
На этапе сборки код проверяется на верность синтаксиса. Незакрытая скобка или неправильное обозначение поля класса немедленно вызовут ошибку при сборке, и бандл просто не соберется. Разработчику придется сразу исправить такие ошибки, чтобы работать дальше.
Также сборщик может подсказать, что какие-то куски кода не используются при выполнении программы. Возможно, это натолкнет разработчика на мысль о более глубоком изучении кода, что косвенно может повлиять на выявление им новых ошибок.
3. Тестирование
Еще один способ не допустить ошибок в коде – тестировать его. Во фронтенде есть инструменты для эффективного использования юнит-тестов. Обычно используют фреймворки, такие как Jest, Karma, Mocha, Jasmine. Вместе с тестовыми фреймворками часто используют расширения, такие как Enzyme, React Testing Library, Sinon и другие, позволяющие обогатить тесты с помощью мокирования, функций-шпионов и других инструментов.
При поиске ошибок тесты в первую очередь полезны нагрузкой разнообразными данными, которые могут привести к ошибкам исполнения. Так, следующий код пройдет валидацию синтаксиса и сработает ожидаемо:
const func = (data) => {
return JSON.parse(data)
}
func('{«a»:1}')
Однако он сломается, если подать ему неверное значение:
func() // Uncaught SyntaxError: Unexpected token u in JSON at position 0.
Этот код тоже проходит валидацию при сборке:
const obj = {
outer : {
last : 9
}
}
if (obj.outer.inner.last) {
console.log(«SUCCESS»)
}
Однако он также сломается во время исполнения. После тестирования разработчик наверняка сделает дополнительные проверки:
if (obj.outer?.inner?.last) {
console.log(«SUCCESS»)
}
Часто подобные ошибки возникают при получение данных от сервера (например AJAX-запросом) с их последующим разбором. Тестирование кода позволяет выявить и заранее устранить случаи, когда код может сломаться во время исполнения в браузере клиента.
4. Логирование
Допустим, мы предприняли все возможные меры по недопущению ошибок во время разработки и сборки проекта. Однако ошибки все-равно могут проникать в продуктивный код. Нам требуется как-то узнавать об их наличии и принимать скорые меры по исправлению. Просить пользователей открывать консоль браузера и делать скриншоты — не самый лучший вариант. Поэтому к проекту неплохо подключить логирование ошибок.
Смысл простой: на каждое событие window.onerror или в каждый переход исполнения кода в блок catch выполняется простой AJAX-запрос на специально выделенный адрес сервера, в тело которого кладется информация об ошибке. Далее потребуется инструмент, который быстро оповестит техподдержку и разработчиков о наличии новых ошибок и позволит эффективно работать с ними. Самый популярный из таких инструментов для фронтенда — Sentry.
Система логирования Sentry позволяет собирать, группировать, представлять ошибки в реальном времени. Есть сборки для разных языков, в том числе JavaScript. Проект предоставляет платный доступ с расширенными возможностями для бизнеса, однако можно попробовать его основные возможности на бесплатном тестовом аккаунте.
Подключать Sentry можно как непосредственно в HTML-файле, так и в компонентах, выполненных на одном из популярных фреймворков: React, Vue, Angular, Ember и других.
Для подключения возможностей логирования прямо в браузере в секции загружаем скрипт:
<script
src=«https://browser.sentry-cdn.com/5.13.0/bundle.min.js»
integrity=«sha384-ePH2Cp6F+/PJbfhDWeQuXujAbpil3zowccx6grtsxOals4qYqJzCjeIa7W2UqunJ»
crossorigin="anonymous"></script>
Далее в коде JS инициализируем:
Sentry.init({
dsn: 'https://<your account key here>@sentry.io/<your project id here>'
});
Всё. Если и когда в коде ниже этой строчки произойдет ошибка, Sentry выполнит ее логирование. Логи будут записаны даже тогда, когда ошибка произошла по вине скриптов с других доменов:
Sentry имеет широкие возможности по анализу массива сообщений об ошибках и настройке уведомлений. Также возможна группировка логов ошибок по релизам вашего продукта:
Sentry.init({
dsn: 'https://<your account key here>@sentry.io/<your project id here>',
release: '2020.03.06.1'
});
С помощью Sentry в статистику можно передать контекст ошибки, например, информацию о клиенте, fingerprint, уровень ошибки (fatal, error, warning, info, debug), проставить теги.
Есть возможность записывать в статистику пользовательские события. Например, можно поставить на отслеживание изменение размеров окна браузера или совершение AJAX-запроса. Также у Sentry есть собственный виджет с окном обратной связи, которое можно показать пользователю при ошибке. Это даст дополнительную информацию для расследования обстоятельств возникновения ошибки.
Для развертывания Sentry вместе с фреймворками достаточно просто установить пакет и подключить:
# Using yarn
yarn add @sentry/browser
# Using npm
npm install @sentry/browser
Делаем инициализацию в главном скрипте проекта (для React и Angular):
import * as Sentry from «@sentry/browser»;
Sentry.init({ dsn: 'https://<your account key here>@sentry.io/<your project id here>' });
Для Vue и Ember передаем еще одну обязательную строку конфигурации:
# Vue
Sentry.init({
dsn: '<your account key here>@sentry.io/<your project id here>',
integrations: [new Integrations.Vue({Vue, attachProps: true})],
});
# Ember
Sentry.init({
dsn: '<your account key here>@sentry.io/<your project id here>',
integrations: [new Integrations.Ember()]
});
Пакет integrations устанавливается отдельно:
# Using yarn
yarn add @sentry/integrations
# Using npm
npm install @sentry/integrations
Для предотвращения конфликтов и дублирования информации при подключении нескольких скриптов в одном проекте Sentry позволяет создавать отдельного клиента для каждой инициализации логирования:
import { BrowserClient } from «@sentry/browser»;
const client = new BrowserClient({
dsn: '<your account key here>@sentry.io/<your project id here>',
});
client.captureException(new Error('example'));
На сайте проекта есть подробная документация с примерами использования: https://docs.sentry.io.
Статья подготовлена при поддержке облачной платформы Mail.ru Cloud Solutions.
Что еще почитать по теме: