Когда мы работаем с внешними данными, возможны самые разные ошибки.
Если приложение сложное, то ошибки естественным образом укладываются в иерархию, разобраться в которой помогает instanceof
.
Свой объект ошибки
Для примера создадим функцию readUser(json)
, которая будет разбирать JSON с данными посетителя. Мы его получаем с сервера – может, нашего, а может – чужого, в общем – желательно проверить на ошибки. А может, это даже и не JSON, а какие-то другие данные – не важно, для наглядности поработаем с JSON.
Пример json
на входе в функцию: { "name": "Вася", "age": 30 }
.
В процессе работы readUser
возможны различные ошибки. Одна – очевидно, SyntaxError
– если передан некорректный JSON.
Но могут быть и другие, например PropertyError
– эта ошибка будет возникать, если в прочитанном объекте нет свойства name
или age
.
Реализуем класс PropertyError
:
function PropertyError(property) {
Error.call(this, property) ;
this.name = "PropertyError";
this.property = property;
this.message = "Ошибка в свойстве " + property;
if (Error.captureStackTrace) {
Error.captureStackTrace(this, PropertyError);
} else {
this.stack = (new Error()).stack;
}
}
PropertyError.prototype = Object.create(Error.prototype);
В этом коде вы можете видеть ряд важных деталей, важных именно для ошибок:
name
– имя ошибки.-
Должно совпадать с именем функции.
message
– сообщение об ошибке.-
Несмотря на то, что
PropertyError
наследует отError
(последняя строка), конструктор у неё немного другой. Он принимает не сообщение об ошибке, а название свойстваproperty
, ну а сообщение генерируется из него.В результате в объекте ошибки есть как стандартное свойство
message
, так и более точноеproperty
.Это частая практика – добавлять в объект ошибки свойства, которых нет в базовых объектах
Error
, более подробно описывающие ситуацию для данного класса ошибок. stack
– стек вызовов, которые в итоге привели к ошибке.-
У встроенных объектов
Error
это свойство есть автоматически, вот к примеру:function f() { alert( new Error().stack ); } f(); // выведет список вложенных вызовов, с номерами строк, где они были сделаны
Если же объект ошибки делаем мы, то «по умолчанию» такого свойства у него не будет. Нам нужно как-то самим узнавать последовательность вложенных вызовов на текущий момент. Однако удобного способа сделать это в JavaScript нет, поэтому мы поступаем хитро и копируем его из нового объекта
new Error
, который генерируем тут же.В V8 (Chrome, Opera, Node.JS) есть нестандартное расширение Error.captureStackTrace, которое позволяет получить стек.
Это делает строка из кода выше:
Error.captureStackTrace(this, PropertyError);
Такой вызов записывает в объект
this
(текущий объект ошибки) стек вызовов, ну а второй аргумент – вообще не обязателен, но если есть, то говорит, что при генерации стека нужно на этой функции остановиться. В результате в стеке будет информация о цепочке вложенных вызовов вплоть до вызоваPropertyError
.То есть, будет последовательность вызовов до генерации ошибки, но не включая код самого конструктора ошибки, который, как правило, не интересен. Такое поведение максимально соответствует встроенным ошибкам JavaScript.
Обычно, когда мы наследуем, то вызываем конструктор родителя. В данном случае вызов выглядит как Error.call(this, message)
.
Строго говоря, этот вызов здесь не обязателен. Встроенный конструктор Error
ничего полезного не делает, даже свойство this.message
(не говоря уже о name
и stack
) не назначает. Единственный возможный смысл его вызова – он ставит специальное внутреннее свойство [[ErrorData]]
, которое выводится в toString
и позволяет увидеть, что это ошибка. Поэтому по стандарту вызывать конструктор Error
при наследовании в таких случаях рекомендовано.
instanceof + try…catch = ♡
Давайте теперь используем наш новый класс для readUser
:
// Объявление
function PropertyError(property) {
this.name = "PropertyError";
this.property = property;
this.message = "Ошибка в свойстве " + property;
if (Error.captureStackTrace) {
Error.captureStackTrace(this, PropertyError);
} else {
this.stack = (new Error()).stack;
}
}
PropertyError.prototype = Object.create(Error.prototype);
// Генерация ошибки
function readUser(data) {
var user = JSON.parse(data);
if (!user.age) {
throw new PropertyError("age");
}
if (!user.name) {
throw new PropertyError("name");
}
return user;
}
// Запуск и try..catch
try {
var user = readUser('{ "age": 25 }');
} catch (err) {
if (err instanceof PropertyError) {
if (err.property == 'name') {
// если в данном месте кода возможны анонимы, то всё нормально
alert( "Здравствуйте, Аноним!" );
} else {
alert( err.message ); // Ошибка в свойстве ...
}
} else if (err instanceof SyntaxError) {
alert( "Ошибка в синтаксисе данных: " + err.message );
} else {
throw err; // неизвестная ошибка, не знаю что с ней делать
}
}
Всё работает – и наша ошибка PropertyError
и встроенная SyntaxError
корректно генерируются, перехватываются, обрабатываются.
Обратим внимание на проверку типа ошибки в try..catch
. Оператор instanceof
проверяет класс с учётом наследования. Это значит, что если мы в дальнейшем решим создать новый тип ошибки, наследующий от PropertyError
, то проверка err instanceof PropertyError
для класса-наследника тоже будет работать. Код получился расширяемым, это очень важно.
Дальнейшее наследование
PropertyError
– это просто общего вида ошибка в свойстве. Создадим ошибку PropertyRequiredError
, которая означает, что свойства нет.
Это подвид PropertyError
, так что унаследуем от неё. Общий вид конструктора-наследника – стандартный:
function PropertyRequiredError(property) {
// вызываем конструктор родителя и передаём текущие аргументы
PropertyError.apply(this, arguments);
...
}
Достаточно ли в наследнике просто вызвать конструктор родителя? Увы, нет.
Если так поступить, то свойство this.name
будет некорректным, да и Error.captureStackTrace
тоже получит неправильную функцию вторым параметром.
Можно ли как-то поправить конструктор родителя, чтобы от него было проще наследовать?
Для этого нужно убрать из него упоминания о конкретном классе PropertyError
, чтобы сделать код универсальным. Частично – это возможно. Как мы помним, существует свойство constructor
, которое есть в prototype
по умолчанию, и которое мы можем намеренно сохранить при наследовании.
Исправим родителя PropertyError
для более удобного наследования от него:
function PropertyError(property) {
this.name = "PropertyError";
this.property = property;
this.message = "Ошибка в свойстве " + property;
if (Error.captureStackTrace) {
Error.captureStackTrace(this, this.constructor); // (*)
} else {
this.stack = (new Error()).stack;
}
}
PropertyError.prototype = Object.create(Error.prototype);
PropertyError.prototype.constructor = PropertyError;
В строке (*)
вместо ссылки на PropertyError
используем constructor
чтобы получить именно конструктор для текущего объекта. В наследнике там будет PropertyRequiredError
, как и задумано.
Мы убрали одну жёсткую привязку к PropertyError
, но со второй (this.name
), увы, сложности. Оно должно содержать имя ошибки, то есть, имя её функции-конструктора. Его можно получить через this.name = this.constructor.name
, но в IE11- это работать не будет.
Если поддерживать IE11-, то тут уж придётся в наследнике его записывать вручную.
Полный код для наследника:
function PropertyRequiredError(property) {
PropertyError.apply(this, arguments);
this.name = 'PropertyRequiredError';
this.message = 'Отсутствует свойство ' + property;
}
PropertyRequiredError.prototype = Object.create(PropertyError.prototype);
PropertyRequiredError.prototype.constructor = PropertyRequiredError;
var err = new PropertyRequiredError("age");
// пройдёт проверку
alert( err instanceof PropertyError ); // true
Здесь заодно и message
в наследнике было перезаписано на более точное. Если хочется избежать записи и перезаписи, то можно оформить его в виде геттера через Object.defineProperty
.
Итого
- Чтобы наследовать от ошибок
Error
, нужно самостоятельно позаботиться оname
,message
иstack
. - Благодаря тому, что
instanceof
поддерживает наследование, удобно организуются проверки на нужный тип. В иерархию ошибок можно в любой момент добавить новые классы, с понятным кодом и предсказуемым поведением.
Чтобы создавать наследники от Error
было проще, можно создать класс CustomError
, записать в него универсальный код, наподобие PropertyError
и далее наследовать уже от него:
// общего вида "наша" ошибка
function CustomError(message) {
this.name = "CustomError";
this.message = message;
if (Error.captureStackTrace) {
Error.captureStackTrace(this, this.constructor);
} else {
this.stack = (new Error()).stack;
}
}
CustomError.prototype = Object.create(Error.prototype);
CustomError.prototype.constructor = CustomError;
// наследник
function PropertyError(property) {
CustomError.call(this, "Ошибка в свойстве " + property)
this.name = "PropertyError";
this.property = property;
}
PropertyError.prototype = Object.create(CustomError.prototype);
PropertyError.prototype.constructor = PropertyError;
// и ещё уровень
function PropertyRequiredError(property) {
PropertyError.call(this, property);
this.name = 'PropertyRequiredError';
this.message = 'Отсутствует свойство ' + property;
}
PropertyRequiredError.prototype = Object.create(PropertyError.prototype);
PropertyRequiredError.prototype.constructor = PropertyRequiredError;
// использование
var err = new PropertyRequiredError("age");
// пройдёт проверку
alert( err instanceof PropertyRequiredError ); // true
alert( err instanceof PropertyError ); // true
alert( err instanceof CustomError ); // true
alert( err instanceof Error ); // true