Зарегистрируйтесь для доступа к 15+ бесплатным курсам по программированию с тренажером

Исключения JS: Введение в ООП

Отдельная большая тема в программировании – обработка ошибок. До сих пор нам удавалось избегать её, но в реальном мире, где приложения содержат тысячи, десятки и сотни тысяч (а то и все миллионы) строк кода, обработка ошибок влияет на многое: простоту модификации и расширения, адекватное поведение программы для пользователя в разных ситуациях.

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

В JavaScript у строк есть метод, который называется text.indexOf(str). Он ищет подстроку str внутри текста text и возвращает индекс начала этой подстроки в тексте. Что произойдёт, если подстрока не была найдена? Является ли это поведение ошибкой? Нет. Это штатное поведение функции. От того, что подстрока не была найдена, ничего страшного не случилось. Представьте себе любой редактор текста и механизм поиска внутри него. Ситуация, когда ничего не было найдено, возникает постоянно, и это не ломает работу программы.

Кстати, посмотрите в документацию этой функции, каким образом она говорит о том, что подстрока не была найдена?

Другая ситуация. В тех же редакторах есть функция "открыть файл". Представьте, что во время открытия файла что-то пошло не так, например, его удалили. А это ошибка или нет? Да, в этой ситуации произошла ошибка, но это не ошибка программирования. Подобная ошибка может возникнуть всегда, независимо от желания программиста. Он не может избежать её появления. Единственное, что он может — правильно реализовать её обработку.

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

Сказанное выше имеет очень серьёзные следствия. Одна и та же ситуация на разных уровнях может как являться ошибкой, так и быть вполне штатной ситуацией. Например, если задача функции читать файл, а она не смогла этого сделать, то с точки зрения этой функции произошла ошибка. Должна ли она приводить к остановке всего приложения? Как мы выяснили выше – не должна. Принимать решение о том, насколько критична данная ситуация, может приложение, которое использует эту функцию, но не сама функция.

Коды возврата

В языках, появившихся до 1990 года (примерно), обработка ошибок выполнялась через механизм возврата функцией специального значения. Например, в Си, если функция не может выполнить свою задачу, то она должна вернуть специальное значение, либо NULL, либо отрицательное число. Значение этого числа говорит о том, какая ошибка произошла. Например:

int write_log()
{
    int ret = 0; // return value 0 if success
    FILE *f = fopen("logfile.txt", "w+");

    // Проверяем, получилось ли открыть файл
    if (!f)
        return -1;

    // Проверяем, что не достигли конца файла
    if (fputs("hello logfile!", f) != EOF) {
        // continue using the file resource
    } else {
        // Файл закончился
        ret = -2;
    }

    // Не получилось закрыть файл
    if (fclose(f) == EOF)
        ret = -3;

    return ret;
}

Обратите внимание на условные конструкции и постоянное присваивание переменной ret. Фактически каждая потенциально опасная операция должна проверяться на успешность выполнения. Если что-то пошло не так, то функция возвращает специальный код.

И вот тут начинаются проблемы. Как показывает жизнь, в большинстве ситуаций ошибка обрабатывается не там, где она возникла, и даже не уровнем выше. Предположим, что есть функция A, которая вызывает код, потенциально приводящий к ошибке, и она его должна уметь правильно обработать и сообщить пользователю о проблеме. При этом сама ошибка происходит внутри функции E, которая вызывается внутри A не напрямую, а через цепочку функций: A => B => C => D => E. Подумайте, к чему приводит такая схема? Все функции в этой цепочке, даже несмотря на то, что они не обрабатывают ошибку, обязаны знать про неё, отлавливать её и так же возвращать наружу код этой ошибки. В итоге кода, который занимается ошибками, становится так много, что за ним теряется код, выполняющий исходную задачу.

Стоит сказать, что существуют схемы обработки ошибок, которые не обладают такими недостатками, но работают по принципу возврата. Например, монада Either.

Исключения

Именно на этом фоне и возник механизм исключений. Его главная цель — передать ошибку из того места, где она возникла, в то место, где её можно обработать, минуя все промежуточные уровни. Другими словами, механизм исключений раскручивает стек вызовов самостоятельно.

С исключениями нужно запомнить две вещи: код, в котором произошла ошибка, выбрасывает исключение, а код, в котором ошибка обрабатывается – его ловит.

// Функция, которая может выбросить исключение
const readFile = (filepath) => {
  if (!isFileReadable(filepath)) {
    // throw – способ выбросить исключение
    throw new Error(`'${filepath}' is not readable`);
  }
  // ...
};

// Где-то в другом месте программы

const run = (filepath) => {
  try {
    // Функция, которая вызывает readFile. Возможно не напрямую, а через другие функции.
    // Для механизма исключений это не важно.
    readFile(filepath);
  } catch (e) {
    // Этот блок выполняется только в одном случае, если в блоке try было выброшено исключение
    // Любая обработка ошибки, например, вывод в консоль
    console.log(e);
  }
  // Если тут будет код, он продолжит выполняться
};

Сами исключения – это объекты Error. Эти объекты содержат внутри себя сообщение, переданное в конструктор, трассировку стека и другие полезные данные.

Самостоятельно исключение выбрасывается с помощью ключевого слова throw:

const e = new Error('Тут любой текст');
throw e; // Исключение можно создать отдельно, а можно сразу же там, где используется throw

Можно выбрасывать не только исключения, но и любые выражения:

const message = 'Тут любой текст';
throw message;

throw прерывает дальнейшее выполнение кода. В этом смысле оно подобно return, но в отличие от него, прерывает выполнение не только текущей функции, но и всего кода, вплоть до ближайшего в стеке вызовов блока catch.

Конструкция try catch - это специальная инструкция из двух блоков, которая позволяет перехватить все исключения и их обработать.

Первый блок формируется после try:

try {
  // ...
}

Любые исключения, которые будут выброшены кодом, расположенным внутри этого блока, будут перехвачены и переданы во второй блок. Если ошибки не было, то этот блок пропускается.

catch (e) {
  // ...
}

Внутри этого блока будет доступна ошибка в переменной e. Можно задать любое имя переменной:

try {
  // ...
} catch (myError) {
  console.log(myError);
}

Внутри блока catch можно выполнять любой код, даже выбрасывать новые исключения, которые могут перехватываться блоком try catch уровнем выше:

const myFunc = () => {
  try {
    // ...
  } catch (e) {
    throw new Error('new error');
  }
};

try {
  myFunc();
} catch (e) {
  console.log(e); // => new error
}

Блок try/catch обычно ставится на самом верхнем уровне программы, но это не обязательно. Вполне вероятно, что есть несколько промежуточных блоков, которые могут отлавливать ошибки и снова их возбуждать. Эта тема достаточно сложная и требует некоторого опыта работы.


Дополнительные материалы

  1. Обработка ошибок
  2. Что такое идентификатор "e", идущий после catch?
  3. Что такое трассировка стека?

Аватары экспертов Хекслета

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят команда поддержки Хекслета или другие студенты

Об обучении на Хекслете

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff
Рекомендуемые программы
профессия
от 6 300 ₽ в месяц
Разработка фронтенд-компонентов для веб-приложений
10 месяцев
с нуля
Старт 21 сентября
профессия
от 6 300 ₽ в месяц
Разработка бэкенд-компонентов для веб-приложений
10 месяцев
с нуля
Старт 21 сентября
профессия
от 10 080 ₽ в месяц
Разработка фронтенд- и бэкенд-компонентов для веб-приложений
16 месяцев
с нуля
Старт 21 сентября
профессия
новый
Автоматизированное тестирование веб-приложений на JavaScript
8 месяцев
c опытом
в разработке
дата определяется

Используйте Хекслет по-максимуму!

  • Задавайте вопросы по уроку
  • Проверяйте знания в квизах
  • Проходите практику прямо в браузере
  • Отслеживайте свой прогресс

Зарегистрируйтесь или войдите в свой аккаунт

Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»