🪲 Ошибки надо возвращать (return), а не выкидывать (throw)
Короче, я раньше к этой теме подходил деликатно, типа "это полезно", "было бы круто, если бы вы делали так", "ну если вы решитесь" и так далее, а сейчас полностью убедился, что это точно верный подход и другого советовать не хочу.
У этого подхода куча преимуществ: (1) вы четко видете какие виды ошибок возвращаете (особенно если создаете кастомные ошибки), (2) нет неявных перехватчиков ошибок, (3) try очень громоздкая и некрасивая конструкция, которая может добавить несколько уровней вложенности и много других полезных пунктов.
Ну и всегда был концептуальный вопрос, которому посвящены множество конференций и статей: почему я должен выбрасывать (throw) ошибку, которая является нормальной частью бизнес-логики (например, NotFound на сущность, которой реально может не быть) при том, что это может убить мою программу, если не будет отловлено.
Что так резко меня убедило?
1. Исторически так было в C. 2. Далее эту фишку подхватил Go. 3. В Rust такая же история, НО они возвращают типа монаду Result<T, Error>. 4. И тут я узнаю, что в Zig (языке, на который я делаю сейчас огрмную ставку) прям встроенный оператор Union Type для возврата ошибок.
Ну, а про функциональные языки даже говорить не буду, там это просто стандарт, тоже через монады.
Если использовать TS, то самый адекватный вариант это: type Result<T, E extends Error = Error> = T | E – могу потом рассказать почему.
Да, есть некоторые места (например, валидация отдельных свойств), где это может сильно засрать код, но и для этого есть решение (об этом в следующих постах)
Мне теперь интересно: насколько вам заходит идея возвращать ошибки, а не выбрасывать их?
Как насчёт трассировки исключений? Если мы делаем return, то ошибки по факту нет
Все зависит от того на каком уровне тебе нужна трассировка
То есть, если это критическая ошибка (например, отвалилась база), то ее можно выкидывать на самый вверх по стэку в общий Error Handler и там будет трассировка
А на уровне обработки return (например, не прошел constraint) можно, например, залогировать
Все зависит от того на каком уровне тебе нужна трассировка
То есть, если это критическая ошибка (например, отвалилась база), то ее можно выкидывать на самый вверх по стэку в общий Error Handler и там будет трассировка
А на уровне обработки return (например, не прошел constraint) можно, например, залогировать
Или я не понял кейса?
Что в твоей системе глобальный хендлер? Я могу предположить 2 варианта: 1. Это что-то рядом с api gateway, что трансформирует ответы на клиента 2. Речь про монолит и мы не учитываем распределённую трассировку. Этот принцип может работать в рамках распределённой системы, как например у нас в Moleculer работает regenerator для воссоздания классов по имплементированной логике из сериализатора (если он их не поддерживает из коробки как cbor-x), тогда по транспорту можно гонять эти ошибки как есть. Создавая дополнительный слой логики, накладных расходов и абстракций. А точно ли нужно это с точки зрения архитектуры?
Прокидывая вызовы дальше, мы увеличиваем стэк ненужными шагами, а он будет лимитирован в любом случае, т.к. нерационально хранить сильно большой трейс. Метрики и трассировка в ноде развивается своим тредом на уровне v8 через channels и будет в дальнейшем из коробки интегрирована в open-telemetry.
Что в твоей системе глобальный хендлер? Я могу предположить 2 варианта: 1. Это что-то рядом с api gateway, что трансформирует ответы на клиента 2. Речь про монолит и мы не учитываем распределённую трассировку. Этот принцип может работать в рамках распределённой системы, как например у нас в Moleculer работает regenerator для воссоздания классов по имплементированной логике из сериализатора (если он их не поддерживает из коробки как cbor-x), тогда по транспорту можно гонять эти ошибки как есть. Создавая дополнительный слой логики, накладных расходов и абстракций. А точно ли нужно это с точки зрения архитектуры?
Прокидывая вызовы дальше, мы увеличиваем стэк ненужными шагами, а он будет лимитирован в любом случае, т.к. нерационально хранить сильно большой трейс. Метрики и трассировка в ноде развивается своим тредом на уровне v8 через channels и будет в дальнейшем из коробки интегрирована в open-telemetry.
Попытаюсь ответить, если я правильно понял:
1. Глобальный Error Handler - это обработчик ошибок, которые мы отлавливаем на уровне триггера (HTTP запрос, Cron, сообщение из MQ, буфер из сокета и т.д.). На этом уровне я могу залогировать / затрейсить ошибку, сериализовать ее и отправить в ответ. 2. Любой узел, получивший в ответ ошибку (клиентское приложение / другой сервис) десериализует ее и может понять что это за ошибка засчет общего словаря кодов ошибок. 3. Далее этот узел принимает решение что делать с ошибкой: если она для него логична, то продолжать исполнение, если нет, то выкинуть ее же ИЛИ модифицированную (например, когда мы не хотим показывать эту ошибку пользователям)
Тем самым, на уровне каждого отдельного узла ты видишь какие у тебя были ошибки и во что они преобразовывались на протяжении сервисов
+ именно трейсами я покрываю только IO и CPU intensive код
Пример: 5-ый по глубине сервис выкинул ошибку отсутствия транзакции, 1-ый, принявший запрос от клиента получит ее, по словарю поймет что это за ошибка и по второму словарю доступных для клиента ошибок поймет что ее надо преобразовать в 403 / 404, в зависимости от политики безопасности. В трейсах будут видны все IO запросы (БД, MQ, HTTP, etc.) с сериализованными в json ошибками (ну, а точнее, с ссылкой на лог, где можно будет посмотреть на эту ошибку)
1. Глобальный Error Handler - это обработчик ошибок, которые мы отлавливаем на уровне триггера (HTTP запрос, Cron, сообщение из MQ, буфер из сокета и т.д.). На этом уровне я могу залогировать / затрейсить ошибку, сериализовать ее и отправить в ответ. 2. Любой узел, получивший в ответ ошибку (клиентское приложение / другой сервис) десериализует ее и может понять что это за ошибка засчет общего словаря кодов ошибок. 3. Далее этот узел принимает решение что делать с ошибкой: если она для него логична, то продолжать исполнение, если нет, то выкинуть ее же ИЛИ модифицированную (например, когда мы не хотим показывать эту ошибку пользователям)
Тем самым, на уровне каждого отдельного узла ты видишь какие у тебя были ошибки и во что они преобразовывались на протяжении сервисов
+ именно трейсами я покрываю только IO и CPU intensive код
Пример: 5-ый по глубине сервис выкинул ошибку отсутствия транзакции, 1-ый, принявший запрос от клиента получит ее, по словарю поймет что это за ошибка и по второму словарю доступных для клиента ошибок поймет что ее надо преобразовать в 403 / 404, в зависимости от политики безопасности. В трейсах будут видны все IO запросы (БД, MQ, HTTP, etc.) с сериализованными в json ошибками (ну, а точнее, с ссылкой на лог, где можно будет посмотреть на эту ошибку)
Про хендлер понял, а вот как ты планируешь трассировать оригинал ошибки в 5 сервисе до преобразования в 1, не очень понял.