1 августа 2019 г.

Свои ошибки, наследование от Error

Материал на этой странице устарел, поэтому скрыт из оглавления сайта.

Более новая информация по этой теме находится на странице https://learn.javascript.ru/custom-errors.

Когда мы работаем с внешними данными, возможны самые разные ошибки.

Если приложение сложное, то ошибки естественным образом укладываются в иерархию, разобраться в которой помогает 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
Карта учебника