Автор Рон Лау, старший инженер-программист HK01

Поскольку последние версии JavaScript (формально EMCAScript) предоставляют нам, программистам, больше программных средств (и синтаксического сахара), код на JavaScript никогда не был более выразительным, хотя появляются новые подводные камни. С развитием инструментов в экосистеме мы можем избежать многих из этих проблем и писать более чистый и понятный код. У нас есть ESLint и его семейство подключаемых модулей для обеспечения согласованного стандарта кодирования и поиска общих проблем со статическим анализом; TypeScript, Flow и другие средства проверки статического типа для обеспечения безопасности типов; Babel для преобразования современного и передового синтаксиса ES в синтаксис, совместимый с вашей целью.

Однако есть одна «вещь», которая преследует нас с незапамятных времен¹, и даже с теми инструментами, которые у нас есть сегодня, все еще необходимо обрабатывать вручную: исключение.

Обычные функции могут вызывать исключение. Конструкторы классов могут вызывать исключения. Методы класса могут вызывать исключения. Генераторы могут вызывать исключения. Awaiting rejected Promises приведет к возникновению исключения. Почти все может вызвать исключение. Статический анализ в этом случае мало помогает, поскольку отслеживание всех возможных мест, которые могут вызвать исключение, может пометить каждую строку вашей кодовой базы. TypeScript тоже не помогает, поскольку спецификация исключения не является частью объявления функции.

Что же тогда делать?

Исключительная точка

Прежде чем углубляться в подробности, давайте поиграем в старую добрую игру в «обнаружение исключения».

Можете ли вы определить, где может возникнуть исключение в следующих файлах TypeScript? Давайте рассмотрим Car класс в черном ящике.

Представьте концепцию безопасности исключений

Безопасность исключений², концепция, обычно связанная с C ++, представляет собой набор свойств, которые указывают, что произойдет, когда в функции возникает исключение, и позволяют пользователю функции размышлять о том, как правильно обработать исключение.

Вообще говоря, гарантии безопасности в исключительных случаях делятся на 4 уровня по возрастанию уровня безопасности:

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

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

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

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

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

Вот и все; нижнего уровня нет. Несоблюдение хотя бы базовой гарантии всегда является ошибкой программы. Правильные программы соответствуют по крайней мере базовой гарантии для всех функций. [Sutter04]

"Разве это не означает, что весь код помещается в try-catch блок?"

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

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

Это означает, что обеспечение безопасности исключений никогда не сводится к простому разбрызгиванию try-catch блоков (или catch обработчиков обещаний), а к чему-то, что является частью дизайна.

Никогда не забывайте о безопасности исключений. Безопасность исключений влияет на дизайн класса. Это никогда не бывает «просто детали реализации». [Sutter99]

Где выбрасываются исключения и когда это небезопасно?

Вернемся к проверке фрагментов кода, выполняемых по функциям, и посмотрим, где возникают исключения.

carHelpers.ts

await fetch(...) на L3 может вызывать, например, когда есть ошибка сети или когда условие на L5 истинно.
Это нарушает безопасность исключения? Нет, поскольку внутреннее состояние не изменяется, даже если возникает исключение.

new Car(...) на L13 может вызвать ошибку.
Обеспечивает ли это безопасность исключения прерывания? Нет, потому что снова внутреннее состояние не изменяется, даже если генерируется исключение. carsCache переназначается только, когда rawCars.map(...) завершается без создания исключения.

Безопасно ли исключение функции? Да, функция имеет «строгую гарантию безопасности исключений».

await getCars() на L6 может вызывать, как мы проиллюстрировали выше.
Обеспечивает ли это безопасность исключения прерывания? Нет. К этому моменту внутреннее состояние не изменяется, и getCars предлагает надежную гарантию безопасности исключений.

car.plateNumber на L10 может скинуть. Для вас это может быть сюрпризом, но car.plateNumber может быть реализован как функция получения, которая может вызывать исключение.
Нарушает ли это безопасность исключений? Да. carByPlateNumberMap будет частично изменен, если в середине цикла возникнет исключение.

Безопасно ли исключение функции? Нет, внутренние состояния будут некогерентными при возникновении исключения на L10. Это пример, когда наличие блока try-catch не делает исключение функции безопасным.

Безопасно ли исключение функции? Да, поскольку в этой функции не возникает никаких исключений. Говорят, что эта функция обеспечивает «гарантию отсутствия выброса».

await getCarByPlateNumberMap() может вызвать ошибку L4, как показано выше.
Нарушает ли это безопасность исключений? Да, getCarByPlateNumberMap не является безопасным в отношении исключений. Если на мгновение предположить, что getCarByPlateNumberMap безопасен в отношении исключений, тогда эта функция также безопасна, поскольку в самой этой функции не изменяется внутреннее состояние.

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

Безопасно ли исключение функции? Да, и оно обеспечивает «гарантию отсутствия исключения».

index.ts

await getCarByPlateNumber(...) может вызвать ошибку L7.
Это нарушает безопасность исключений? Да, поскольку getCarByPlateNumber не безопасен в отношении исключений.

Присваивание car.owner может вызвать L8, поскольку car.owner может иметь функцию установки, которая может генерировать исключение.
Это нарушает безопасность исключений? Нет. Внутренние состояния остаются неизменными.

Присваивание car.plateNumber может вызвать L9, также из-за возможной реализации установщика.
Нарушает ли это безопасность исключений? Да и нет. Тем не менее, car.owner здесь изменен. Если внутреннее состояние car по-прежнему согласовано (инвариант не нарушен), мы можем считать, что эта программа имеет «базовую гарантию безопасности исключений», в противном случае это небезопасное исключение.

Методы обеспечения безопасности исключений

Сузить круг обязанностей функции

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

Помните, что «отсутствие ошибок» и «плохой дизайн» идут рука об руку: если часть кода сложно заставить удовлетворять даже базовые гарантии, это почти всегда является признаком плохой разработки. Например, функцию, имеющую несколько не связанных между собой обязанностей, трудно сделать безошибочной. [Sutter04]

«Клонировать и назначать»

Клонируйте объект / значение / состояние программы, которое вы собираетесь изменить (или создать новый экземпляр), работать с копией и заменить старое значение.

Возьмем для примера getCarByPlateNumberMap:

Используйте встроенные неизменяющие функции

ES5 предоставляет ряд неизменяющих функций, таких как Array.prototype.filter, Array.prototype.map и Array.prototype.reduce⁴. Array.prototype.slice был предоставлен ES3⁵. Эти функции возвращают новый экземпляр вместо изменения исходного объекта.

Снова возьмем для примера getCarByPlateNumberMap. С Array.prototype.map мы можем также устранить for-of-loop:

Обе реализации будут обеспечивать «надежную гарантию безопасности исключений».

Библиотека функционального программирования / библиотека неизменяемой структуры данных

Неизменяемость данных - это основная концепция функционального программирования. В JS есть библиотеки, которые обеспечивают функциональное программирование, например, Bacon.js, fp-ts (только TypeScript) и Ramda. Для неизменяемых структур данных Immutable.js и Immer - две популярные библиотеки, предоставляющие такие возможности.

Предоставьте самую надежную гарантию безопасности, которая не будет наказывать вызывающих абонентов, которым она не нужна [Sutter04]

Не обязательно всегда предоставлять надежную гарантию безопасности. Иногда это даже невозможно.

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

Вывод

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

Большое спасибо за чтение.

[1]: Именно из 3-го издания EMCAScript, выпущенного в декабре 1999 г. и вошедшего в широкое использование с выпуском Internet Explorer 5.5 в июле 2020 г., в котором в язык были введены throw и try...catch. Если мы посчитаем «ошибки времени выполнения», генерируемые встроенными функциями³, которые не могут быть обработаны, то, вероятно, это будет из самой первой версии JavaScript.

[2]: Впервые введено Дэвидом Арбрахамсом в Безопасность исключений в STLport в 1996 году и расширено в Уроки, извлеченные из определения безопасности исключений для стандартной библиотеки C ++.

[3]: Раздел 5.2, P.8, EMCA-262 1-е издание

[4]: Раздел 15.4.4.19–15.4.4.21, P.135–138, EMCA-262 5-е издание

[5]: Раздел 15.4.4.10, P.93, EMCA-262, 3-е издание

[Sutter99]: Херб Саттер, Exceptional C ++: 47 инженерных задач, проблем программирования и решений. Аддисон-Уэсли, 1999.

[Sutter04]: Херб Саттер и Андрей Александреску, Стандарты программирования C ++: 101 правила, рекомендации и передовой опыт. Аддисон-Уэсли, 2004.

Спасибо, что прочитали, хлопайте 👏, если вам это нравится. Принимаем на работу, описание вакансий можно найти ЗДЕСЬ.