Когда мы что-то разрабатываем, нам часто нужны собственные классы ошибок, чтобы отражать конкретные вещи, которые могут пойти не так в наших задачах. Для ошибок в сетевых операциях нам может понадобиться HttpError
, для операций с базой данных DbError
, для операций поиска NotFoundError
и так далее.
Наши ошибки должны поддерживать основные свойства ошибок, такие как message
, name
и, желательно, stack
. Но они также могут иметь и другие собственные свойства, например, объекты HttpError
могут иметь свойство statusCode
со значением, например, 404
, 403
или 500
.
JavaScript позволяет использовать throw
с любым аргументом, поэтому технически нашим пользовательским классам ошибок не нужно наследовать от Error
. Но если мы наследуем, то становится возможным использовать obj instanceof Error
для идентификации объектов ошибок. Поэтому лучше наследовать от него.
По мере роста приложения наши собственные ошибки естественным образом образуют иерархию. Например, HttpTimeoutError
может наследовать от HttpError
и т. д.
В качестве примера рассмотрим функцию readUser(json)
, которая должна читать JSON с пользовательскими данными.
Вот пример того, как может выглядеть действительный json
:
let json = `{ "name": "Джон", "возраст": 30 }`;
Внутри мы будем использовать JSON.parse
. Если он получает неверный json
, он выдает SyntaxError
. Но даже если json
синтаксически правильный, это не значит, что это действительный пользователь, верно? Он может пропустить необходимые данные. Например, у него может не быть свойств name
и age
, которые важны для наших пользователей.
Наша функция readUser(json)
будет не только читать JSON, но и проверять («проверять») данные. Если обязательных полей нет или формат неправильный, то это ошибка. И это не SyntaxError
, поскольку данные синтаксически верны, а ошибка другого рода. Мы назовем его ValidationError
и создадим для него класс. Ошибка такого рода также должна содержать информацию о проблемном поле.
Наш класс ValidationError
должен наследовать от класса Error
.
Класс Error
встроен, но вот его приблизительный код, чтобы мы могли понять, что мы расширяем:
// «Псевдокод» для встроенного класса Error, определенного самим JavaScript класс Ошибка { конструктор (сообщение) { это.сообщение = сообщение; this.name = "Ошибка"; // (разные имена для разных встроенных классов ошибок) this.stack = <стек вызовов>; // нестандартно, но большинство сред поддерживают его } }
Теперь унаследуем от него ValidationError
и попробуем его в действии:
класс ValidationError расширяет ошибку { конструктор (сообщение) { супер (сообщение); // (1) this.name = "Ошибка проверки"; // (2) } } функция тест() { throw new ValidationError("Упс!"); } пытаться { тест(); } поймать (ошибиться) { оповещение (ошибка.сообщение); // Упс! оповещение(ошибка.имя); // Ошибка проверки оповещение(err.stack); // список вложенных вызовов с номерами строк для каждого }
Обратите внимание: в строке (1)
мы вызываем родительский конструктор. JavaScript требует, чтобы мы вызывали super
в дочернем конструкторе, так что это обязательно. Родительский конструктор устанавливает свойство message
.
Родительский конструктор также устанавливает для свойства name
значение "Error"
, поэтому в строке (2)
мы возвращаем ему правильное значение.
Давайте попробуем использовать его в readUser(json)
:
класс ValidationError расширяет ошибку { конструктор (сообщение) { супер (сообщение); this.name = "Ошибка проверки"; } } // Использование функция readUser(json) { пусть пользователь = JSON.parse(json); если (!user.age) { throw new ValidationError("Нет поля: возраст"); } если (!user.name) { throw new ValidationError("Нет поля: имя"); } вернуть пользователя; } // Рабочий пример с try..catch пытаться { let user = readUser('{ "age": 25 }'); } поймать (ошибиться) { если (ошибка экземпляра ValidationError) { alert("Неверные данные: " + err.message); // Неверные данные: Нет поля: имя } else if (err instanceof SyntaxError) { // (*) alert("Синтаксическая ошибка JSON: " + err.message); } еще { выбросить ошибку; // неизвестная ошибка, выдать ее повторно (**) } }
Блок try..catch
в приведенном выше коде обрабатывает как ValidationError
, так и встроенную SyntaxError
из JSON.parse
.
Пожалуйста, взгляните, как мы используем instanceof
для проверки конкретного типа ошибки в строке (*)
.
Мы также могли бы посмотреть на err.name
, например:
// ... // вместо (ошибка экземпляра SyntaxError) } else if (err.name == "SyntaxError") { // (*) // ...
Версия instanceof
намного лучше, потому что в будущем мы собираемся расширить ValidationError
, создав его подтипы, например PropertyRequiredError
. И проверка instanceof
продолжит работать для новых наследующих классов. Так что это перспективно.
Также важно, что если catch
встречает неизвестную ошибку, он повторно выдает ее в строке (**)
. Блок catch
знает только, как обрабатывать ошибки проверки и синтаксиса, другие виды (вызванные опечаткой в коде или другими неизвестными причинами) должны не пройти.
Класс ValidationError
очень общий. Многие вещи могут пойти не так. Свойство может отсутствовать или иметь неверный формат (например, строковое значение age
вместо числа). Давайте создадим более конкретный класс PropertyRequiredError
именно для отсутствующих свойств. Он будет содержать дополнительную информацию об отсутствующем имуществе.
класс ValidationError расширяет ошибку { конструктор (сообщение) { супер (сообщение); this.name = "Ошибка проверки"; } } класс PropertyRequiredError расширяет ValidationError { конструктор(свойство) { super("Нет свойства: " + свойство); this.name = "PropertyRequiredError"; this.property = свойство; } } // Использование функция readUser(json) { пусть пользователь = JSON.parse(json); если (!user.age) { бросить новый PropertyRequiredError("возраст"); } если (!user.name) { бросить новый PropertyRequiredError("имя"); } вернуть пользователя; } // Рабочий пример с try..catch пытаться { let user = readUser('{ "age": 25 }'); } поймать (ошибиться) { если (ошибка экземпляра ValidationError) { alert("Неверные данные: " + err.message); // Неверные данные: нет свойства: имя оповещение(ошибка.имя); // Ошибка свойстваRequired оповещение(ошибка.свойство); // имя } Еще если (ошибка экземпляра SyntaxError) { alert("Синтаксическая ошибка JSON: " + err.message); } еще { выбросить ошибку; // неизвестная ошибка, выдать ее повторно } }
Новый класс PropertyRequiredError
прост в использовании: нам нужно только передать имя свойства: new PropertyRequiredError(property)
. Удобочитаемое message
генерируется конструктором.
Обратите внимание, что this.name
в конструкторе PropertyRequiredError
снова назначается вручную. Это может оказаться немного утомительным — присваивать this.name = <class name>
в каждом пользовательском классе ошибок. Мы можем избежать этого, создав собственный класс «базовой ошибки», который присваивает this.name = this.constructor.name
. А затем унаследовать от него все наши пользовательские ошибки.
Назовем это MyError
.
Вот упрощенный код с MyError
и другими пользовательскими классами ошибок:
класс MyError расширяет ошибку { конструктор (сообщение) { супер (сообщение); это.имя = это.конструктор.имя; } } класс ValidationError расширяет MyError { } класс PropertyRequiredError расширяет ValidationError { конструктор(свойство) { super("Нет свойства: " + свойство); this.property = свойство; } } // имя правильное alert(new PropertyRequiredError("поле").name ); // Ошибка свойстваRequired
Теперь пользовательские ошибки стали намного короче, особенно ValidationError
, так как мы избавились от строки "this.name = ..."
в конструкторе.
Цель функции readUser
в приведенном выше коде — «прочитать пользовательские данные». В процессе могут возникать различные ошибки. Сейчас у нас есть SyntaxError
и ValidationError
, но в будущем функция readUser
может вырасти и, возможно, генерировать другие виды ошибок.
Код, вызывающий readUser
должен обрабатывать эти ошибки. Прямо сейчас он использует несколько if
в блоке catch
, которые проверяют класс, обрабатывают известные ошибки и повторно выдают неизвестные.
Схема такая:
пытаться { ... readUser() // потенциальный источник ошибки ... } поймать (ошибиться) { если (ошибка экземпляра ValidationError) { // обрабатываем ошибки проверки } Еще если (ошибка экземпляра SyntaxError) { // обрабатываем синтаксические ошибки } еще { выбросить ошибку; // неизвестная ошибка, выдать ее повторно } }
В приведенном выше коде мы видим два типа ошибок, но их может быть и больше.
Если функция readUser
генерирует несколько видов ошибок, то нам следует спросить себя: действительно ли мы хотим каждый раз проверять все типы ошибок по одному?
Часто ответ — «Нет»: нам хотелось бы быть «на уровень выше всего этого». Мы просто хотим знать, произошла ли «ошибка чтения данных» — почему именно это произошло, часто не имеет значения (это описано в сообщении об ошибке). Или, что еще лучше, мы хотели бы иметь возможность получить подробную информацию об ошибке, но только в том случае, если нам это необходимо.
Техника, которую мы здесь описываем, называется «обертывание исключений».
Мы создадим новый класс ReadError
для представления общей ошибки «чтения данных».
Функция readUser
будет перехватывать возникающие внутри нее ошибки чтения данных, такие как ValidationError
и SyntaxError
, и вместо этого генерировать ReadError
.
Объект ReadError
сохранит ссылку на исходную ошибку в своем свойстве cause
.
Тогда код, вызывающий readUser
должен будет проверять только ReadError
, а не все виды ошибок чтения данных. А если ему требуется более подробная информация об ошибке, он может проверить свойство cause
.
Вот код, который определяет ReadError
и демонстрирует его использование в readUser
и try..catch
:
класс ReadError расширяет ошибку { конструктор (сообщение, причина) { супер (сообщение); это.причина = причина; this.name = 'Ошибка чтения'; } } класс ValidationError расширяет ошибку { /*...*/ } класс PropertyRequiredError расширяет ValidationError { /* ... */ } функция validateUser (пользователь) { если (!user.age) { бросить новый PropertyRequiredError("возраст"); } если (!user.name) { бросить новый PropertyRequiredError("имя"); } } функция readUser(json) { пусть пользователь; пытаться { пользователь = JSON.parse(json); } поймать (ошибиться) { если (ошибка экземпляра SyntaxError) { throw new ReadError("Синтаксическая ошибка", err); } еще { выбросить ошибку; } } пытаться { проверитьПользователь(пользователь); } поймать (ошибиться) { если (ошибка экземпляра ValidationError) { throw new ReadError("Ошибка проверки", err); } еще { выбросить ошибку; } } } пытаться { readUser('{плохой json}'); } поймать (е) { если (e экземпляр ReadError) { предупреждение(е); // Исходная ошибка: SyntaxError: Неожиданный токен b в JSON в позиции 1 alert("Исходная ошибка: " + e.cause); } еще { бросить е; } }
В приведенном выше коде readUser
работает точно так, как описано — перехватывает ошибки синтаксиса и проверки и вместо этого выдает ошибки ReadError
(неизвестные ошибки выдаются повторно, как обычно).
Таким образом, внешний код проверяет instanceof ReadError
и все. Нет необходимости перечислять все возможные типы ошибок.
Этот подход называется «обертывание исключений», поскольку мы берем исключения «низкого уровня» и «обертываем» их в ReadError
, который является более абстрактным. Он широко используется в объектно-ориентированном программировании.
Обычно мы можем наследовать от Error
и других встроенных классов ошибок. Нам просто нужно позаботиться о свойстве name
и не забыть вызвать super
.
Мы можем использовать instanceof
для проверки определенных ошибок. Это также работает с наследованием. Но иногда у нас есть объект ошибки, поступающий из сторонней библиотеки, и нет простого способа получить его класс. Тогда для таких проверок можно использовать свойство name
.
Обертывание исключений — широко распространенный метод: функция обрабатывает исключения низкого уровня и создает ошибки более высокого уровня вместо различных ошибок низкого уровня. Исключения низкого уровня иногда становятся свойствами этого объекта, например err.cause
в приведенных выше примерах, но это не является строго обязательным.
важность: 5
Создайте класс FormatError
, который наследуется от встроенного класса SyntaxError
.
Он должен поддерживать свойства message
, name
и stack
.
Пример использования:
let err = new FormatError("ошибка форматирования"); предупреждение (ошибка.сообщение); // ошибка форматирования предупреждение(ошибка.имя); //Ошибка формата предупреждение(ошибка.стек); // куча предупреждение (ошибка экземпляра FormatError); // истинный предупреждение (ошибка экземпляра SyntaxError); // true (потому что наследуется от SyntaxError)
класс FormatError расширяет SyntaxError { конструктор (сообщение) { супер (сообщение); это.имя = это.конструктор.имя; } } let err = new FormatError("ошибка форматирования"); предупреждение (ошибка.сообщение); // ошибка форматирования предупреждение(ошибка.имя); //Ошибка формата предупреждение(ошибка.стек); // куча предупреждение (ошибка экземпляра SyntaxError); // истинный