DEV Community

Savchenko Alexander
Savchenko Alexander

Posted on • Updated on

99% ошибок мы показываем пользователю

Серверная часть

Часть 1

99% ошибок мы показываем пользователю. Обычно мы выводим их в уведомлении или под невалидным полем. Что же тогда получается? Если мы 99% ошибок показываем пользователю, то может быть стоит создать для них единый формат? Как для бэка, так и для фронта?
Если да, то какие поля будут в объекте ошибки и какие значения принимать?

Давайте создадим класс BaseError и расширим им класс Error. Первое что в ней точно будет так это унаследованное поле message.
Это ошибка для разработчика и логирования! Пользователю мы ее никогда выводить не будем!

Для вывода сообщений пользователю нам понадобится поле _errorCode.
Пример значений:

  1. uniqConstraint
  2. formatConstraint
  3. connection

Теперь значение этого поля мы сможем смапить с сообщениями на фронте:

  1. Ошибка уникальности
  2. Неверный формат
  3. Не удалось подключиться к базе
{
  "_message": "Db error",
  "_errorCode": "connection"
  "_status": 500
}
Enter fullscreen mode Exit fullscreen mode

Это необходимый минимум

Часть 2

Что если нам надо вывести название поля, в котором произошла ошибка?
Добавим поле _key.
А какое значение при этом было?
Добавим поле _value.
Я бы назвал эту ошибку CollectableError.
Так же добавим необязательное поле errors в BaseError, так как нам надо куда-то складывать наши CollectableErrorы.

{
  "_message": "Validation error",
  "_errorCode": "invalid",
  "_status": 401,
  "_errors": {
    "username": {
      "_key": "username",
      "_value": undefined,
      "_errorCode": "validateNotUndefined",
      "_message": "required"
    },
    "password": {
      "_key": "password",
      "_value": 100,
      "_errorCode": "validateString",
      "_message": "not string" 
    }
  }

}
Enter fullscreen mode Exit fullscreen mode

Выводим:

  1. Поле "username" обязательно
  2. Формат "password" должен быть строкой, а получено число

Видите! Нам теперь message вообще не нужен! Мы теперь с легкостью сами можем сгенерировать нужный нам текст.

Я специально не стал класть CollectableError в массив, а положил в обьект по ключу дублирующий key: так намного удобнее получать доступ к ошибке на фронте.

Часть 3

Часто при ошибке валидации нам нужно вывести пользователю значение которое мы ожидаем от него. Это может быть проверка на максимальное/минимальное количество символов или паттерн.
Для этого добавим необязательные поля с абстрактным именем _key2 и _value2.
В маленьком проекте _key2 скорее всего будет принимать лишь несколько имен: pattern, max, min.

{
  "_message": "Validation error",
  "_errorCode": "invalid",
  "_status": 500,
  "_errors": {
    "username": {
      "_key": "username",
      "_value": "nick",
      "_key2": "min",
      "_value2": 5,
      "_errorCode": "validateNotLessThan",
      "_message": "too less symbols"
    },
    "email": {
      "_key": "email",
      "_value": "nick@com",
      "_key2": "pattern",
      "_value2": "@.*?.",
      "_errorCode": "validateByPattern",
      "_message": "does not match the pattern"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Выводим:

  1. Поле "username" должно содержать как минимум 5 символов, получено 4
  2. Поле "email" должно соответствовать паттерну: @.*?.

Клиентская часть

А теперь самое интересное.

Видели последние два примера? Что если валидаторы, которые выкидывают эти ошибки перенести на фронт и начать их использовать? Минимум лишних телодвижений! И если вдруг вы забудете добавить валидатор на клиенте и он сработает на сервере, то всё отработает так будто ничего и не случилось, потому что с сервера прилетит такой же errorCode и мапиться он будет так же.

Такой подход позволяет:

  1. Поддерживать несколько языков (весь текст в ошибках должен быть на английском, мапа с языками хранится на фронте)
  2. Хранить все сообщения в одном месте
  3. Не отвлекаться при разработке бизнес логики на выдумывание что написать в сообщении, чтобы было ТОЧНО понятно
  4. Этих данных более чем достаточно чтобы пользователю ТОЧНО было понятно что произошло
  5. Всё ваше приложение в курсе формата вашей ошибки
  6. Простота

Когда же стоит выкидывать Error? Наверное только когда вы не сможете вывести это сообщение пользователю и не сможете обработать ошибку.

Дополнительно

Перехват ошибок

Для перехвата прочих ошибок (базы, языка) нам понадобится написать мидлвар для koa или фильтр для nest. В них мы будем конвертировать неугодный нам формат ошибок в наш собственный

Вложенные ошибки

Вложенность ошибок я бы посоветовал делать исходя из формата вложенности вашей библиотеки стэйта формы.
Пример для final-form:

{
   "_errors": { 
     "address": {
       "address": {
       "postcode": {
         "_key": "postcode",
         "_value": 446303,
         "_errorCode": "validateString"
       }
     }
   }
}
Enter fullscreen mode Exit fullscreen mode

ОГРАНИЧЕНИЯ: если ваше поле будет иметь имя "key", то оно будет конфликтовать с полем ошибки "key", поэтому стоит избегать такого названия или именовать поле в ошибке как "_key".

Временно оставлю ссылочку на то что у меня получилось на сервере https://github.com/savchenko91/auth-server/blob/master/src/utils/errors.ts

Top comments (0)