Продолжая свою последнюю статью, я хочу поговорить об ошибках. Ошибки - это хорошо - я уверен, вы слышали это раньше. На первый взгляд мы боимся Ошибок, потому что они часто включают в себя обиду или чувство унижения на публике. Выполняя Ошибки, мы фактически узнаем, как не что-то делать и как сделать это лучше в следующий раз.

Очевидно, что речь идет об учении на ошибках (ошибках) в реальной жизни. Ошибки в программировании немного другие. Они предоставляют нам полезные функции, позволяющие улучшить наш код и сообщить пользователю, когда что-то не так (и, возможно, также научить его, как это исправить).

Эта статья состоит из 3 частей, сначала мы рассмотрим ошибки в целом. После этого мы сосредоточимся на бэкэнде (Node.js + Express.js) и, наконец, увидим, как бороться с ошибками в React.js. Я выбрал эти фреймворки, потому что они, безусловно, самые популярные на данный момент, но вы сможете легко применить свои новые знания к другим фреймворкам!

Полный пример проекта доступен на github.

I. Ошибки JavaScript и общая обработка

throw new Error('something went wrong') - создаст экземпляр ошибки в JavaScript и остановит выполнение вашего скрипта, если вы не сделаете что-то с ошибкой. Когда вы начинаете свою карьеру в качестве разработчика JavaScript, вы, скорее всего, не сделаете этого сами, но вместо этого вы видели, как это делают другие библиотеки (или среда выполнения), например ReferenceError: fs is not defined или аналогичный.

Объект ошибки

У объекта Error есть два встроенных свойства, которые мы можем использовать. Первое - это сообщение, которое вы передаете в качестве аргумента конструктору ошибки, например. new Error('This is the message'). Вы можете получить доступ к сообщению через свойство message:

const myError = new Error(‘please improve your code’)
console.log(myError.message) // please improve your code

Второй, очень важный, - это трассировка стека ошибок. Вы можете получить к нему доступ через свойство `stack`. Стек ошибок предоставит вам историю (стек вызовов) того, какие файлы были «ответственны» за возникновение этой ошибки. Стек также включает сообщение вверху, за которым следует фактический стек, начиная с самой последней / изолированной точки ошибки и идя вниз до самого внешнего «ответственного» файла:

Error: please improve your code
 at Object.<anonymous> (/Users/gisderdube/Documents/_projects/hacking.nosync/error-handling/src/general.js:1:79)
 at Module._compile (internal/modules/cjs/loader.js:689:30)
 at Object.Module._extensions..js (internal/modules/cjs/loader.js:700:10)
 at Module.load (internal/modules/cjs/loader.js:599:32)
 at tryModuleLoad (internal/modules/cjs/loader.js:538:12)
 at Function.Module._load (internal/modules/cjs/loader.js:530:3)
 at Function.Module.runMain (internal/modules/cjs/loader.js:742:12)
 at startup (internal/bootstrap/node.js:266:19)
 at bootstrapNodeJSCore (internal/bootstrap/node.js:596:3)

Выбрасывание и обработка ошибок

Теперь один только экземпляр Error ничего не вызывает. Например. new Error('...') ничего не делает. Когда ошибка становится throw n, становится немного интереснее. Затем, как было сказано ранее, ваш скрипт перестанет выполняться, если вы каким-то образом не обработаете его в своем процессе. Помните, не имеет значения, если вы throw an Ошибка вручную, она вызвана библиотекой или даже самой средой выполнения (узлом или браузером). Давайте посмотрим, как мы можем справиться с этими ошибками в различных сценариях.

try .... catch

Это самый простой, но часто забытый способ обработки ошибок - в наши дни он снова используется гораздо чаще, благодаря async / await, см. Ниже. Это можно использовать для обнаружения любой синхронной ошибки. Пример:

Если мы не заключим console.log(b) в блок try… catch, выполнение скрипта остановится.

… наконец

Иногда необходимо выполнить код в любом случае, независимо от того, есть ошибка или нет. Для этого вы можете использовать третий необязательный блок finally. Часто это то же самое, что просто поставить строку после оператора try… catch, но иногда это может быть полезно.

Асинхронность - обратные вызовы

Асинхронность - одна тема, которую вы всегда должны учитывать при работе с JavaScript. Когда у вас есть асинхронная функция, и внутри этой функции возникает ошибка, ваш скрипт уже продолжит выполнение, поэтому сразу не возникнет какой-либо ошибки. При обработке асинхронных функций с обратными вызовами (кстати, не рекомендуется) вы обычно получаете два параметра в своей функции обратного вызова, которые выглядят примерно так:

Если есть ошибка, параметр err будет равен этой ошибке. Если нет, параметр будет undefined или null. Важно либо вернуть что-то в if(err) -блоке, либо заключить другую инструкцию в else -блок, иначе вы можете получить другую ошибку, например result может быть неопределенным, и вы пытаетесь получить доступ к result.data или аналогичному.

Асинхронность - обещания

Лучший способ справиться с асинхронностью - использовать обещания. Здесь, помимо более удобочитаемого кода, мы также улучшили обработку ошибок. Нам больше не нужно так сильно заботиться о точном обнаружении ошибок, пока у нас есть блок catch. При связывании обещаний блок catch улавливает все ошибки с момента выполнения обещания или последнего блока захвата. Обратите внимание, что обещания без catch-block не завершат сценарий, но предоставят вам менее читаемое сообщение, например

(node:7741) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): Error: something went wrong
(node:7741) DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code. */

Поэтому всегда добавляйте к своим обещаниям блокировку. Давайте посмотрим:

попробуй ... поймай - снова

С введением async / await в JavaScript мы вернулись к исходному способу обработки ошибок, с помощью try… catch… finally, что упрощает их обработку:

Поскольку это тот же способ, который мы использовали для обработки «обычных» синхронных ошибок, при желании проще иметь операторы catch с более широкой областью действия.

II. Создание и обработка ошибок на сервере

Теперь, когда у нас есть инструменты для работы с ошибками, давайте посмотрим, что мы можем с ними делать в реальных ситуациях. Создание ошибок и их правильная обработка в серверной части - важная часть вашего приложения. Существуют разные подходы к устранению ошибок. Я покажу вам подход с настраиваемым конструктором ошибок и кодами ошибок, которые мы можем легко передать вашему интерфейсу или любому потребителю API. То, как вы структурируете свой бэкэнд в деталях, не имеет большого значения, идея остается той же.

Мы будем использовать Express.js в качестве среды маршрутизации. Давайте подумаем о структуре, в которой мы хотим иметь наиболее эффективную обработку ошибок. Мы хотим:

  1. Общая обработка ошибок, своего рода запасной вариант, который в основном просто говорит: «Что-то пошло не так, попробуйте еще раз или свяжитесь с нами». Это не особенно умно, но, по крайней мере, уведомляет пользователя о том, что что-то не так - вместо бесконечной загрузки или чего-то подобного.
  2. Специальная обработка ошибок, чтобы предоставить пользователю подробную информацию о том, что не так и как это исправить, например отсутствует какая-то информация, запись уже существует в базе данных и т. д.

Создание настраиваемого конструктора ошибок

Мы воспользуемся существующим конструктором Error и расширим его. Наследование в JavaScript - дело рискованное, но в данном случае я убедился, что это очень полезно. Зачем нам это нужно? Мы по-прежнему хотим, чтобы трассировка стека была удобной для отладки. Расширение встроенного конструктора ошибок JavaScript дает нам бесплатную трассировку стека. Единственное, что мы добавляем, - это code, к которому позже мы можем получить доступ через err.code, а также статус (код статуса http) для передачи во внешний интерфейс.

Как справиться с маршрутизацией

Когда наша настраиваемая ошибка готова к использованию, нам нужно настроить структуру маршрутизации. Как я уже отмечал, нам нужна единая точка истины для обработки ошибок, то есть для каждого маршрута мы хотим иметь одно и то же поведение при обработке ошибок. По умолчанию express не поддерживает это, поскольку все маршруты инкапсулированы.

Чтобы решить эту проблему, мы можем реализовать обработчик маршрута и определить нашу фактическую логику маршрута как обычные функции. Таким образом, в случае, если функция маршрута (или любая функция внутри) выдает ошибку, она будет возвращена обработчику маршрута, который затем может передать ее во внешний интерфейс. Каждый раз, когда в бэкэнде возникает ошибка, мы хотим передать ответ во фронтенд - предполагая JSON API - в следующем формате:

{
    error: 'SOME_ERROR_CODE',
    description: 'Something bad happened. Please try again or     contact support.'
}

Приготовьтесь к поражению. Мои ученики всегда злились на меня, когда я говорил:

Ничего страшного, если ты не все понимаешь с первого взгляда. Просто воспользуйтесь им, и через некоторое время вы поймете, почему это имеет смысл.

Кстати, это также называется нисходящим обучением, и я его большой поклонник.

Так выглядит сам обработчик маршрута:

Надеюсь, вы можете прочитать комментарии в коде, я подумал, что это имеет больше смысла, чем объяснение здесь. Теперь давайте посмотрим, как выглядит настоящий файл маршрута:

В этих примерах я ничего не делаю с фактическим запросом, я просто имитирую различные сценарии ошибок. Так, например, GET /city окажется в строке 3, POST /city окажется в строке 8 и так далее. Это также работает с параметрами запроса, например. GET /city?startsWith=R. По сути, у вас будет либо необработанная ошибка, которую интерфейс получит как

{
    error: 'GENERIC',
    description: 'Something went wrong. Please try again or contact support.'
}

или вы выбросите CustomError вручную, например

throw new CustomError('MY_CODE', 400, 'Error description')

который превращается в

{
    error: 'MY_CODE',
    description: 'Error description'
}

Теперь, когда у нас есть эта прекрасная настройка бэкэнда, у нас больше нет журналов ошибок, просачивающихся во фронтенд, и мы всегда будем возвращать полезную информацию о том, что пошло не так.

Обязательно посмотрите полное репо на github. Не стесняйтесь использовать его для любых своих проектов и модифицировать под свои нужды!

III. Отображение ошибок пользователю

Следующим и последним шагом является управление ошибками во внешнем интерфейсе. Здесь вы хотите обрабатывать ошибки, вызванные самой логикой вашего внешнего интерфейса, с помощью инструментов, описанных в первой части. Однако ошибки из бэкэнда также должны отображаться. Давайте сначала посмотрим, как мы можем отображать ошибки. Как упоминалось ранее, в нашем пошаговом руководстве мы будем использовать React.

Сохранение ошибок в состоянии React

Так же, как и другие данные, ошибки и сообщения об ошибках могут изменяться, поэтому вы хотите поместить их в состояние ваших компонентов. По умолчанию и при подключении вы хотите сбросить ошибки, чтобы при первом просмотре страницы пользователем ошибки не отображались.

Следующее, что нам нужно прояснить, - это разные типы Ошибок с соответствующим визуальным представлением. Как и в бэкэнде, есть 3 типа:

  1. Глобальные ошибки, например одна из наших общих ошибок возвращается из серверной части, или пользователь не вошел в систему и т. д.
  2. Конкретные ошибки, исходящие от серверной части, например пользователь отправляет свои учетные данные для входа в серверную часть. Серверная часть отвечает, что пароль не совпадает. Это не может быть проверено внешним интерфейсом, поэтому оно должно исходить от внутреннего интерфейса.
  3. Конкретные ошибки, вызванные самим интерфейсом, например проверка ввода электронной почты не выполняется.

2. и 3. очень похожи и могут обрабатываться в одном и том же состоянии (при желании), но имеют разное происхождение. Мы увидим в коде, как это разыграется.

Я собираюсь использовать собственную реализацию состояния React, но вы также можете использовать системы управления состоянием, такие как MobX или Redux.

Глобальные ошибки

Обычно я сохраняю эти ошибки в внешнем компоненте с отслеживанием состояния и визуализирую статический элемент пользовательского интерфейса, это может быть красный баннер в верхней части экрана, модальное окно или что-то еще, реализация дизайна зависит от ты.

Посмотрим на код:

Как видите, у нас есть ошибка в состоянии Application.js. У нас также есть методы для сброса и изменения значения ошибки. Мы передаем значение и метод сброса компоненту GlobalError, который позаботится об его отображении и сбросе при нажатии на «x». Посмотрим, как выглядит GlobalError-компонент:

Как вы можете видеть в строке 5, мы ничего не визуализируем, если нет ошибки. Это предотвращает постоянное наличие пустого красного поля на нашей странице. Конечно, вы можете изменить внешний вид и поведение этого компонента. Например, вы можете заменить «x» на Timeout, который сбрасывает состояние ошибки через пару секунд.

Теперь вы готовы использовать это глобальное состояние ошибки где угодно, просто передайте _setError из Application.js, и вы можете установить глобальную ошибку, например когда запрос от бэкэнда возвращается с полем error: 'GENERIC'. Пример:

Если вы ленивы, можете остановиться на этом. Даже если у вас есть определенные ошибки, вы всегда можете просто изменить глобальное состояние ошибки и отобразить поле ошибки вверху страницы. Однако я собираюсь показать вам, как обрабатывать и отображать определенные ошибки. Почему? Во-первых, это подробное руководство по обработке ошибок, поэтому я не могу останавливаться на достигнутом. Во-вторых, UX-люди, вероятно, взбесятся, если вы просто отобразите все ошибки глобально.

Обработка конкретных ошибок запроса

Подобно глобальным ошибкам, мы можем иметь локальное состояние ошибки и внутри других компонентов. Порядок действий такой же:

Следует помнить, что очистка ошибки обычно имеет другой триггер. Не имеет смысла использовать символ «x» для удаления ошибки. Здесь было бы разумнее сбросить ошибку при создании нового запроса. Вы также можете сбросить ошибку, когда пользователь вносит изменения, например при изменении входного значения.

Ошибки происхождения веб-интерфейса

Как упоминалось ранее, эти ошибки можно обрабатывать таким же образом (состояние), как и определенные ошибки, исходящие от серверной части. Давайте на этот раз воспользуемся примером с полем ввода и позволим пользователю удалять город только в том случае, если он фактически ввел данные:

Ошибка интернационализации с использованием кода ошибки

Возможно, вам было интересно, почему у нас есть эти коды ошибок, например GENERIC, мы просто отображали описание ошибки, переданное из бэкэнда. Теперь, когда ваше приложение вырастет, вы, надеюсь, завоюете новые рынки и в какой-то момент столкнетесь с проблемой поддержки нескольких языков. Если вы находитесь в этой точке, вы можете использовать упомянутый код ошибки, чтобы отобразить правильный заголовок на языке пользователя.

Надеюсь, вы получили представление о том, как бороться с ошибками. Быстро набираемый и столь же быстро забываемый console.error(err) теперь должен уйти в прошлое. Это важно использовать для отладки, но это не должно попадать в вашу производственную сборку. Чтобы предотвратить это, я бы порекомендовал вам использовать библиотеку протоколирования, я использовал loglevel в прошлом, и мне это очень нравится.

Об авторе: Лукас Гисдер-Дубе стал соучредителем и руководил стартапом в качестве технического директора в течение полутора лет, создавая техническую команду и архитектуру. Покинув стартап, он преподавал программирование в качестве ведущего инструктора в Ironhack, а сейчас создает стартап-агентство и консалтинговое агентство в Берлине. Посетите dube.io, чтобы узнать больше.