Выбрасывание исключительных ситуаций в JavaScript

В предыдущих уроках мы с вами изучили два места, в которых JavaScript выбрасывает исключение в случае каких-то проблем.

В вашем проекте, однако, могут быть и другие ситуации, которые для вас являются исключительными, а для JavaScript - нет. В таком случае мы можете создавать и выбрасывать свои, пользовательские исключения.

Давайте разберем синтаксис, необходимый для этого. Для начала исключение нужно создать с помощью команды new Error, параметром передав текст исключения:

new Error('текст исключения');

Затем это исключение нужно выбросить с помощью команды throw:

throw new Error('текст исключения');

Выбрасывание исключение заставляет JavaScript считать, что случилась исключительная ситуация. Это значит, что такое исключение можно отловить с помощью конструкции try-catch и обработать нужным образом.

Давайте посмотрим на примере, как этим пользоваться. Пусть у нас есть функция, которая делит одно число на другое:

function div(a, b) { return a / b; }

Давайте будем считать, что деление на ноль запрещено и при попытке сделать это должна возникать исключительная ситуация. Для этого будем проверять в функции, нет ли попытки делить на 0. Если нет - будем делить, а если нет - будем бросать исключение:

function div(a, b) { if (b !== 0) { return a / b; } else { throw new Error('ошибка деления на ноль'); } }

Давайте для начала просто попробуем поделить на 0, не перехватывая исключение:

alert( div(3, 0) );

В этом случае выполнение скрипта прервется и в консоли появится ошибка с текстом 'ошибка деления на ноль' (проверьте). Давайте теперь будем перехватывать нашу ошибку и как-то ее обрабатывать:

try { alert( div(3, 0) ); } catch (error) { alert('вы пытаетесь делить на 0, что запрещено'); }

В JavaScript попытка извлечь корень из отрицательного числа не приводит к выбрасыванию исключения:

let result = Math.sqrt(-1); console.log(result); // выведет NaN

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

Типы исключений

Давайте выбросим свое исключение и посмотрим, как будет вести себя объект с ошибкой в этом случае:

try { throw new Error('текст исключения'); } catch (error) { console.log(error.name); // 'Error' console.log(error.message); // 'текст исключения' }

Как вы видите, тип нашего исключения - 'Error'. Такой же тип будет у любых исключений, выброшенных подобным образом. Это, однако, не всегда будет удобно, так как, если у нас могут быть несколько исключений, мы не сможем их отличить друг от друга.

В JavaScript предусмотрено решение этой проблемы: можно выбрасывать исключения не только типа Error, но и любого встроенного в JavaScript типа ошибки, например, TypeError, SyntaxError, RangeError.

Давайте для примера выбросим исключение типа SyntaxError:

try { throw new SyntaxError('текст исключения'); } catch (error) { console.log(error.name); // 'SyntaxError' console.log(error.message); // 'текст исключения' }

Выбросите исключение с типом TypeError.

Выбросите исключение с типом SyntaxError и RangeError. Поймайте эти исключения с помощью одного блока try. В блоке catch выведите разные сообщения об ошибке для исключений разных типов.

Свои типы исключений

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

Существуют разные способы сделать это. Самый простой - в throw передать объект с ключами name и message:

try { throw {name: 'MyError', message: 'текст исключения'}; } catch (error) { console.log(error.name); // 'MyError' console.log(error.message); // 'текст исключения' }

Выше я мы сделали функцию, выбрасывающую исключение при делении на ноль:

function div(a, b) { if (b !== 0) { return a / b; } else { throw new Error('ошибка деления на ноль'); } }

Переделайте эту функцию так, чтобы она выбрасывала исключение с каким-нибудь придуманными нами типом, например, DivisionByZeroError.

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

Пример применения

Пусть при загрузке страницы сервер создает HTML код, в котором хранится название, цена и количество купленного продукта:

<div id="product" data-product="яблоко" data-price="1000" data-amount="5"></div>

Давайте сделаем функцию, которая будет принимать ссылку на элемент с продуктом и находить полную стоимость товара (цену умножать на количество):

function getCost(elem) { return elem.dataset.price * elem.dataset.amount; }

Найдем стоимость нашего продукта:

let product = document.querySelector('#product'); let cost = getCost(product); alert(cost);

Предположим теперь следующую ситуацию: из-за какого-то сбоя на сервере он прислал нам товар, в котором отсутствует цена или количество (или оба сразу), например, вот так:

<div id="product" data-product="яблоко" data-price="1000"></div>

Если теперь попробовать посчитать стоимость товара, то результате на экран выведется NaN. Согласитесь, не очень информативно.

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

function getCost(elem) { if (elem.dataset.price !== undefined && elem.dataset.amount !== undefined) { return elem.dataset.price * elem.dataset.amount; } else { return 0; // вернем что-нибудь, например, 0 или null или false } }

Второй вариант - это сказать, что отсутствие атрибута data-price или data-amount - исключительная ситуация. В этом случае мы будем выбрасывать исключение:

function getCost(elem) { if (elem.dataset.price !== undefined && elem.dataset.amount !== undefined) { return elem.dataset.price * elem.dataset.amount; } else { throw { name: 'ProductCostError', message: 'отсутствует цена или количество у продукта' }; } }

Какой из двух вариантов здесь уместнее применить - это выбор программиста. Он может считать проблему нормальной работой скрипта или исключительной ситуацией.

Пусть мы решили, что ситуация исключительная. Тогда код получения стоимости товара будет выглядеть вот так:

let product = document.querySelector('#product'); try { let cost = getCost(product); alert(cost); } catch (error) { // как-то реагируем на исключение }

Переделайте мой код так, чтобы функция getCost выбрасывала два типа исключений: если отсутствует цена и если отсутствует количество. Хорошо подумайте над названиями этих исключений. В блоке catch выведите разные сообщения об ошибке для исключений разных типов.

Еще пример применения

Пусть к нам откуда-то из внешнего мира приходит JSON с продуктом:

let json = '{"product": "яблоко", "price": 1000, "amount": 5}'; let product = JSON.parse(json); alert(product.price * product.amount);

Вы уже знаете, что метод JSON.parse будет выбрасывать исключение, если JSON некорректный. Давайте поймаем это исключение:

try { let json = '{"product": "яблоко", "price": 1000, "amount": 5}'; let product = JSON.parse(json); alert(product.price * product.amount); } catch (error) { // как-то реагируем на исключение }

Однако, может быть такое, что сам по себе JSON корректный, но не содержит нужных нам полей, например, нет поля с ценой:

let json = '{"product": "яблоко", "amount": 5}'; // нет цены

Давайте скажем, что это тоже исключительная ситуация и будем в таком случае выбрасывать свое пользовательское исключение:

try { let json = '{"product": "яблоко", "amount": 5}'; let product = JSON.parse(json); if (product.price !== undefined && product.amount !== undefined) { alert(product.price * product.amount); } else { throw { name: 'ProductCostError', message: 'отсутствует цена или количество у продукта' }; } } catch (error) { // как-то реагируем на исключение }

Теперь блок catch будет получать два типа исключений: либо JSON вообще некорректен, и тогда будет исключение типа SyntaxError, либо JSON корректен, но не содержит нужных нам полей, и тогда будет исключение типа ProductCostError.

Давайте в блоке catch будем отлавливать эти типы исключений:

try { let json = '{"product": "яблоко", "amount": 5}'; let product = JSON.parse(json); if (product.price !== undefined && product.amount !== undefined) { alert(product.price * product.amount); } else { throw { name: 'ProductCostError', message: 'отсутствует цена или количество у продукта' }; } } catch (error) { if (error.name == 'SyntaxError') { alert('Некорректный JSON продукта'); } else if (error.name == 'ProductCostError') { alert('У продукта отсутствует цена или количество'); } }

Пусть к вам приходит JSON вот такого вида:

let json = `[ { "name": "user1", "age": 25, "salary": 1000 }, { "name": "user2", "age": 26, "salary": 2000 }, { "name": "user3", "age": 27, "salary": 3000 } ]`;

Проверьте этот JSON на общую корректность при разборе, а после разбора проверьте, что в результате получается массив, а не что-то другое. Если в результате получается не массив - выбросите исключение.

Проброс исключений

Рассмотрим блок catch задачи о JSON продукта:

catch (error) { if (error.name == 'SyntaxError') { alert('Некорректный JSON продукта'); } else if (error.name == 'ProductCostError') { alert('У продукта отсутствует цена или количество'); } }

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

Когда я говорю, что не будет никакой реакции, то имею ввиду, что реально никакой: даже не будет вываливания ошибки в консоль. Наш код просто молча не будет работать.

Поэтому существует следующее правило: ваш код должен ловить только те исключения, с которыми знает, как справится. Если исключение не известное, то его нужно пробросить дальше с помощью throw. В этом случае его выше поймает кто-то более осведомленный либо исключение вывалится ошибкой в консоль.

Давайте поправим наш код:

catch (error) { if (error.name == 'SyntaxError') { alert('Некорректный JSON продукта'); } else if (error.name == 'ProductCostError') { alert('У продукта отсутствует цена или количество'); } else { throw error; // пробрасываем исключение далее } }

Дан следующий код:

try { let arr = JSON.parse(json); for (let i = 0; i < arr.length; i++) { localStorage.setItem(i, arr[i]); } } catch (error) { if (error.name == 'QuotaExceededError') { alert('закончилось место в хранилище'); } if (error.name == 'SyntaxError') { alert('некорректный JSON'); } }

Что не так с этим кодом? Исправьте его на более удачный.