99% ошибок мы показываем пользователю
Published: 2022-01-11 06:40:43

# Серверная часть
## Часть 1

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

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

Для вывода сообщений пользователю нам понадобится поле `_errorCode`. 
Пример значений:
1. uniqConstraint
2. formatConstraint
3. connection

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

```json
{
  "_message": "Db error",
  "_errorCode": "connection"
  "_status": 500
}
```

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

## Часть 2

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

```json
{
  "_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" 
    }
  }

}
```

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

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

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

## Часть 3

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

```json
{
  "_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"
    }
  ]
}
```

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


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

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

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

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

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

# Дополнительно
## Перехват ошибок

Для перехвата прочих ошибок (базы, языка) нам понадобится написать мидлвар для `koa` или [фильтр](https://docs.nestjs.com/exception-filters) для `nest`. В них мы будем конвертировать неугодный нам формат ошибок в наш собственный

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

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

```json
{
   "_errors": { 
     "address": {
       "address": {
       "postcode": {
         "_key": "postcode",
         "_value": 446303,
         "_errorCode": "validateString"
       }
     }
   }
}
```

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

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