Привет, Хабр! Меня зовут Владимир, я фронтенд-разработчик в Rusprofile. Давайте поговорим о работе с исключениями.

Ситуация: пишешь код на JS, хочешь обработать исключение, пробрасываешь throw, ловишь его с помощью try-catch. Но добавим нюанс: пусть это нужно сделать для setTimeout. Эта функция использует окружения браузера – не JS-функция, асинхронная. 

// Не сработает
try {
  setTimeout(() => {
    throw new Error('Ошибка');
  }, 0);
} catch (e) {
  // Этот код никогда не выполнится
  console.error(e);
}

Загвоздка: catch не сможет поймать ошибку.

Этот пример – одна из нескольких особенностей JS, из-за которых я считаю обработку исключений “из коробки” на этом языке неудобной. Но есть и хорошие новости – для JS существуют альтернативные способы работы с исключениями, с которыми дела обстоят получше; например, паттерн “контейнерный тип”.

Давайте разбираться.

Это база 

Итак, у нас в коде что-то может пойти не так (увы). Какие-то ситуации могут нарушать фундаментальные контракты системы – не закрыли скобку, зациклили функцию, переполнили память. Приложение ложится, и это в целом правильно. 

Другие ситуации менее критичны для системы: вызвали не тот метод, обратились не к тому свойству объекта.

Ситуации второго типа мы называем исключениями, хотим уметь их находить и исправлять (обрабатывать), без падения приложения. 

Как и в большинстве языков программирования, исключение в JS опираются на использование оператора throw, значения которой мы ловим с помощью конструкции try-catch. 

Через throw можно описать все некритичные исключительные ситуации в коде. Значение throw может быть любым – строкой, числом, другой функцией.

Есть класс Error, который имеет множество подклассов и довольно удобный API, а также содержит полезную метаинформацию для отладки. 

Есть финализаторы (finally), чтобы даже если у нас произошла ошибка, мы в любом случае могли выполнить код (закрыть соединение к БД, убрать лоадер и так далее). Внутри finally могут возникать новые исключения.

Что не так с исключениями на JS

Изначально JS создавался как встраиваемый язык программирования, который зависит от окружения, в нашем случае браузера. Язык синхронный by design; конструкция try-catch изначально была рассчитана на синхронный код. 

Для примера из начала статьи неотловленное исключение двинется за пределы функции, “наверх”, сея на своем пути разрушения. Приложение упадет. 

В какой-то момент в JS появился метод async/await – с его помощью мы можем сделать функцию асинхронной и внутри нее уже вызвать try-catch. 

Есть класс AggregateError, который позволяет сгруппировать несколько ошибок, используется в специфических сценариях (например, Promise.allSettled), но не является мейнстримным решением для обработки исключений.

// Async / await, AggregateError
async function multipleErrors() {
  const promises = [
    Promise.reject(new Error('Ошибка 1')),
    Promise.reject(new Error('Ошибка 2')),
    Promise.resolve('Успех')
  ];

  try {
    const settled = await Promise.allSettled(promises);
    const errors = settled.filter((r) => r.status === 'rejected').map((r) => r.reason);

    if (errors.length) {
      throw new AggregateError(errors, 'Несколько ошибок');
    }
  } catch (e) {
    console.error(e); // ['Ошибка 1','Ошибка 2']
  }
}

Но неудобств всё равно хватает.

В JS у нас нет возможности типизировать исключения. Для произвольной функции в коде мы не можем сказать, вернёт ли она исключение или нет. Да, у нас есть описания в JSDoc, и мы можем надеяться, что наша IDE туда заглянет и потому подскажет нам, что прописанную в JSDoc функцию нужно завернуть в try-catch. 

/**
 * @throws {RangeError} может быть исключение
 */
function onMathRandom() {
  const number = Math.random();

  // ...какая-то логика

  if (number > 0.5) {
    return number;
  } else {
    throw new RangeError('Ошибка');
  }
}

Но это не железобетонно надежный вариант. 

Если мы не отловили исключение, то дальше код не будет работать; приложение упадёт.

Ещё в JS отсутствует синтаксис типа catch (TypeError | RangeError), поэтому приходится делать вложенные try/catch или городить проверку типа в одном catch через instanceof. Это может стать проблемой в плохо организованном коде или в следствии плохих архитектурных решений.

Что делать: контейнеры

Нам на помощь могут прийти паттерны из мира функционального программирования – такие, как контейнерный тип. Мы можем создать контейнер по типу promise, и в нем обрабатывать исключения с помощью обычных return. Это обертка, которая добавляет контекст данным.

Контейнер не позволяет обратиться к данным напрямую. У него есть API, который возвращает методы, через которые мы потом получаем нужные нам данные. 


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

Ещё одно преимущество контейнерного типа – типизация на TS. Если мы используем контейнер, то нам в целом не нужно знать, как контейнер написан или даже как работает – при наведении на контейнер TS покажет всё нужное (какие данные вернет контейнер, какие там есть методы).

Пример: пусть у нас есть функция openFile. 

function openFile(path: string) {
  if (!path) return Result.onError('Укажите валидный путь');

  return Result.onSuccess({ name: 'file', path });
}

openFile('/test.txt').then(console.log).catch(console.error);

Значение функции – строка. Если строки нет или она неправильная, то возвращаем контейнер с ошибкой. 

В метод onError мы передаем сообщение “Укажите валидный путь”. Если мы можем открыть файл, то есть путь правильный, мы возвращаем контейнер с сообщением об успехе. Туда помещаем объект – имя файла и путь к нему.

Дальше мы вызываем эту функцию и передаем туда какую-то строку. У нас есть два метода: then и catch. В then мы передаем функцию, которая примет данные при успешном выполнении; catch обработает ошибку, если она произошла. 

Таким образом, у нас не будет ситуации, что мы не обработали ни тот, ни другой метод. Нам не нужно это оборачивать в try-catch. Удобно! 

С контейнерным типом мы не можем забыть обработать исключение – взаимодействуем с API контейнера.

Это отлично работает на TS: не нужно вчитываться в код, чтобы понять, что у значения есть контекст; всё подскажет IDE.  

Из минусов: контейнерный тип бывает многословен, может выглядеть громоздко, напрашиваться на рефакторинг. Это поправимо; в этом случае на помощь могут прийти декораторы

У тебя есть задача – придумай, как её решить 

Работа с исключениями на JS может быть неудобной, “так сложилось исторически”. Но это обстоятельство на мой взгляд привносит в работу фронтенд-разработчика не только фрустрацию, но и дополнительный элемент творчества.

Варианты, как обойти неудобства существуют – контейнерный тип, а ещё константы. Например, как в функции indexOf() – если индекс не найден, нам вернется отрицательное число в виде -1, что является тоже своего рода обработкой исключительной ситуации.

Варианты есть, нужно только поискать; заглянуть в соседние языки программирования и паттерны оттуда; попробовать.

Исключения на JS – неудобно? Возможно. Повод узнать что-то новое? Определенно! 

Комментарии (2)


  1. parus-lead
    04.09.2025 10:50

    Хорошо описано. Кстати, очень полезный пример, поскольку несколько раз сам сталкивался с проблемами исключений на JS. Надо попробовать


  1. Vindicar
    04.09.2025 10:50

    Эта функция использует окружения браузера – не JS-функция, асинхронная. 

    Ну так почему не использовать async/await? Тогда и обычный try-catch сработает. Если речь идёт вот именно про setTimeout(), его тоже можно завернуть в промис.

    Я соглашусь, что монада Either (а ваш контейнер - это оно) имеет свои преимущества, но пример-то можно было подобрать и понагляднее.