Изучение принципов распространения ошибок
Одной из самых сомнительных концепций в JavaScript и TypeScript является распространение ошибок. Частью проблемы является непонимание разницы между исключением и ошибкой.
Этот пост направлен на обнаружение типов исключений и определение принципов распространения ошибок. Несмотря на то, что в статье особое внимание уделяется JavaScript и TypeScript, те же принципы распространения ошибок применимы ко многим другим языкам.
Исключения и ошибки
Ошибка — это объект, содержащий информацию о том, что пошло не так и где это произошло в коде. Исключения не являются ошибками; исключения – это аномальные или исключительные условия, требующие специальной обработки. Есть два типа таких условий: работающие и нерабочие.
Ошибки проверки ввода – это операционныеошибки. Неудачная попытка входа — это рабочая ситуация. Эти варианты использования ожидаются и обрабатываются соответствующим образом. Приложение продолжает работать в обычном режиме всякий раз, когда происходят такие сценарии.
Нерабочее состояние – это когда приложение не может автоматически устранить ошибку и должно быть остановлено. Например, приложение должно хранить данные в БД. Часть функционала приложения теряется при невозможности подключения к БД. Если эта функция критична, приложение находится в нерабочем состоянии. Если он не может автоматически восстановиться, он должен быть прекращен.
Распространение ошибок
Все начинается с распространения ошибок. Метод, с помощью которого возвращается ошибка, определяет, может ли приложение продолжать работу или его лучше остановить. Метод распространения ошибки также определяет, как следует обрабатывать ошибку.
Существует два способа распространения ошибки в JavaScript и TypeScript:
- Выбросить исключение. Он завершает процесс, если не обрабатывается. Его следует использовать, когда намерение состоит в том, чтобы остановить приложение, когда что-то пойдет не так.
- Вернуть ошибку. Он обозначает ожидаемый сценарий возникновения ошибки и не требует завершения работы приложения.
Опасность использования механизма throw для возврата ошибки заключается в том, что он может завершить работу приложения, если не будет обработан должным образом. Просто возврат ошибок не остановит приложение. Но игнорирование таких ошибок может привести приложение в неожиданное и неупорядоченное состояние.
Еще одним важным аспектом при выборе механизма распространения ошибок является документация. При работе с TypeScript языковой сервис выводит типы входных и выходных параметров функций. Если функция возвращает параметр типа ошибки, он виден в редакторе кода и снижает риск пропуска обработки ошибок. Ситуация отличается, когда функция выдает исключение — языковой сервис не может его идентифицировать. В TSDoc определен тег throws , но он широко не используется. Единственный способ узнать, что функция выдает исключение, — прочитать ее исходный код.
Обработка исключений
Обработка исключений зависит от того, как возвращается ошибка, и от типа функции, которая возвращает ошибку. Это относительно просто для синхронных функций. Ошибка проверяется с помощью предложения if в случае, если она возвращается функцией. Если функция выдает исключение, ее вызов обычно защищен с помощью try/catch.
Обработка исключений более сложна в случае асинхронных функций. Есть несколько способов обработки асинхронных операций:
- Используйте функции обратного вызова
- Используйте промисы с
.thenи.catch - Используйте
awaitдля разрешения промисов
Есть еще генераторы, но я их опущу.
Обработка исключений в обратных вызовах довольно проста. Всякий раз, когда асинхронная операция завершается, обработчик операции вызывает функцию обратного вызова и возвращает ошибки, если таковые имеются. По соглашению первым параметром функции обратного вызова является ошибка. Если ошибка не нулевая, вызов функции обратного вызова останавливается. Это упрощает понимание потока управления.
Здесь есть несколько проблем. Неясно, является ли ошибка результатом рабочего или нерабочего исключения. Другой проблемой является распространение результатов обработки. Необходимость передавать выходные данные одной функции на вход другой привела к созданию большого количества кода с вложенными обратными вызовами, который трудно читать и поддерживать.
Промисы решили проблему организации последовательных вызовов функций (синхронных и асинхронных). Последовательность обещаний может быть объединена в одном вызове с помощью .then, и данные могут передаваться между вызовами. Но обещания используют обратные вызовы и наследуют те же проблемы обработки исключений.
Async/await устранил необходимость использования обратных вызовов и решил проблему оркестровки. Теперь можно ожидать результата асинхронной функции. Это делает код чище и легче для чтения, по крайней мере, счастливый путь. Try/catch заменил метод обработки ошибок обратного вызова. Сначала казалось, что это победа. На самом деле новые проблемы заменили старые проблемы:
- При защите вызова асинхронной функции с помощью try/catch по-прежнему неясно, является ли ошибка следствием операционного или неоперационного исключения.
- Нередко можно увидеть несколько вызовов асинхронных функций в одном блоке try/catch. В этом случае предложение catch обрабатывает исключения из всех функций. Также нередко можно увидеть сложную логику обработки ошибок в блоке catch в попытке понять, какой вызов функции является источником ошибки.
- Вложенные блоки try/catch. Это выглядит еще хуже, чем вложенные обратные вызовы.
Итак, как мы можем использовать преимущества async/await в оркестровке асинхронных вызовов и свести к минимуму проблемы с обработкой исключений?
Распространение ошибок и принципы обработки
- Возвращает ошибку для рабочихисключений.
- Всегда проверяйте возвращенные ошибки.
- Выдавать ошибку для нерабочих исключений.
- Блок операторов Try должен защищать один логический блок или вызов.
- Избегайте сложной логики в предложении catch.
- Не вкладывать блоки try/catch.
- Ошибки переноса.
Оборачивать ошибки означает брать одно значение ошибки и помещать в него другое значение ошибки. Это позволяет расширить ошибку дополнительной информацией о том, откуда она возникла или как это произошло, без потери исходного значения. Есть несколько библиотек, поддерживающих перенос ошибок в JavaScript. В качестве альтернативы вы можете использовать ECMAScript2022 со встроенной поддержкой error.cause.
В следующих разделах показаны примеры распространения и обработки ошибок:
Попробуйте поймать подход
Легко рассуждать о коде, когда try/catch защищает один оператор. Ситуация быстро ухудшается, когда необходимо обрабатывать несколько асинхронных вызовов.
Обертывание try/catch
Этот подход скрывает try/catch внутри функции. В объявлении функции указано, что она может возвращать ошибку или значение. Он позволяет использовать оператор if для управления логическим потоком.
Следующий пример — это следующий шаг к обобщению обработки ошибок. Он использует служебные типы TypeScript для вывода параметров функций и типов вывода.
В приведенных выше примерах используется API чтения файлов Deno. Чтобы запустить их, используйте следующую команду:
$ FILE=hello.txt deno run --allow-env=FILE --allow-read main.ts
Спасибо за прочтение.