Перейти к содержимому
Теория · Ошибки · урок 7 из 10

Свои классы ошибок

В цепочке поручений бывают записки двух сортов. Одни сообщают о том, с чем помощник справится сам: «клиент не указал имя — подставь „гость“». Другие сообщают о настоящей поломке, и её должен увидеть начальник. Чтобы не перепутать, свои записки помощник пишет на собственном бланке и узнаёт их с первого взгляда.

В программе без такого бланка ошибки неотличимы. Функция checkName бросает Error, когда имя пустое. Но в том же try может случиться и ошибка в самой программе, и catch получит обе одинаково:

JavaScript
function checkName(name) {
    if (name.length === 0) {
        throw new Error("name is empty");
    }
    return name;
}

try {
    checkName(null);
} catch (e) {
    console.log(e.message); // Cannot read properties of null (reading 'length')
}

Давайте разберём, что случилось:

  1. checkName(null) пытается прочитать name.length, а у null свойств нет.
  2. JavaScript сам бросает TypeError, и catch его ловит.
  3. catch печатает текст встроенной ошибки — это текст Node и Chrome.

Для "" блок catch получил бы текст name is empty, а для null — TypeError из чтения name.length. Первая ошибка значит «пользователь не ввёл имя», вторая — «в программе баг». Проверка instanceof Error верна для обеих, а e.name у первой равно "Error", как у любой другой ошибки из new Error. Сравнивать текст message ненадёжно: его меняют при правке программы, а встроенное сообщение из примера выше другие браузеры пишут не так, как Node и Chrome.

Класс ошибки

Собственный бланк — это свой вид ошибки: класс, который наследует Error.

JavaScript
class ValidationError extends Error {
    constructor(message) {
        super(message);
        this.name = "ValidationError";
    }
}

const e = new ValidationError("name is empty");
console.log(e.name); // ValidationError
console.log(e.message); // name is empty
console.log(e instanceof ValidationError, e instanceof Error); // true true

Разберём класс по частям:

  1. extends Error делает ValidationError видом ошибки.
  2. Вызов super(message) передаёт текст конструктору Error, и тот записывает его в message.
  3. Строка this.name = "ValidationError" задаёт вид ошибки, который вернёт e.name.

Экземпляр такого класса — по-прежнему Error, поэтому для него верна и проверка instanceof Error. В конструкторе можно записать и свои свойства, например this.field = "name", чтобы запомнить, какое поле формы заполнено неверно.

Поймать свою ошибку

Теперь проверка пустого имени бросает new ValidationError("name is empty") вместо new Error. Блок catch по-прежнему получает любую ошибку, но обрабатывает только этот вид, а любую другую бросает дальше:

JavaScript
function nameOrGuest(name) {
    try {
        if (name.length === 0) {
            throw new ValidationError("name is empty");
        }
        return name;
    } catch (e) {
        if (e instanceof ValidationError) {
            return "guest";
        }
        throw e;
    }
}

console.log(nameOrGuest("Ann"), nameOrGuest("")); // Ann guest

Давайте разберём оба вызова:

  1. nameOrGuest("Ann") проходит проверку и возвращает "Ann", а catch не выполняется.
  2. nameOrGuest("") бросает ValidationError.
  3. catch получает её, проверка instanceof ValidationError верна, и функция возвращает "guest".

Выбор делает if внутри catch, а не сам catch: ловит он всё, что брошено в try.

Здесь аналогия с бланком перестаёт работать. Свой бланк помощник узнаёт с первого взгляда, а catch узнаёт свою ошибку только по проверке instanceof, которую вы пишете сами.

Бросить дальше

Строка throw e бросает пойманный объект ещё раз, без изменений. Вызов nameOrGuest(null) не превратит баг в гостя: чтение name.length бросит TypeError, и тот выйдет наружу. Если выше его никто не поймает, программа остановится.

Без строки throw e баг пропал бы молча: функция вернула бы undefined, и никто бы о нём не узнал. Ловят только те ошибки, которые код умеет обработать, а остальные пропускают дальше.

На собеседовании об этом спрашивают так: «Зачем создавать свои классы ошибок?» Ответ: чтобы catch отличал ожидаемую ошибку от бага через instanceof и не глотал чужие ошибки.

Частая ошибка

В конструкторе забыли задать name:

JavaScript
class ValidationError extends Error {
    constructor(message) {
        super(message); // ✗
    }
}

Сообщения об ошибке не будет, и проверка instanceof ValidationError по-прежнему работает. Но e.name не называет вид ошибки: name экземпляра — "Error", унаследованный от родителя. Код, который различает ошибки по e.name, примет такую ошибку за обычную Error. После super(message) нужна строка this.name = "ValidationError";.

Главное

  • Свой вид ошибки — класс, который наследует Error и задаёт this.name после super(message).
  • catch получает любую ошибку, а свою он узнаёт через instanceof.
  • Ошибку, которую код обработать не может, бросают дальше через throw e.

Подпишись, чтобы пройти курс

Курс бесплатный. Войди через Telegram и подпишись на канал — откроются все уроки.

Войти другим способом