Gernar
Frontend DeveloperJavaScript, асинхронность

Интервью-вопрос

Что принимает метод reject в Promise

reject принимает одну причину отклонения Promise. На практике важно не только знать, что тип может быть любым, но и объяснить, почему в приложении обычно передают Error.

Добавлен
Редакция

Подготовьте короткий ответ и пару деталей на случай уточняющих вопросов.

🐰0
🥚0

Мини-квиз

Проверка перед разбором

Несколько быстрых вопросов перед разбором. Так проще поймать места, которые только кажутся понятными.

Вопрос 1 из 50 правильно

Что лучше ответить про аргумент reject?

Вы хотите дать короткий и точный ответ на интервью.

Варианты ответа

Разбор

Разобраться, а не зазубрить

Дальше разбираем суть, типичные уточнения и места, где легко сказать лишнее или перепутать термины.

Базовая идея

У Promise есть два финальных состояния: fulfilled и rejected. reject(reason) переводит Promise в состояние rejected. Переданное значение становится причиной отклонения. Это значение обычно называют reason.

На интервью стоит сказать аккуратно: JavaScript не требует, чтобы reason был именно Error. Но если вы передаете строку или произвольный объект, вы сами отвечаете за то, как потом это обработать, залогировать и показать пользователю.

Хороший короткий ответ:

reject принимает один аргумент, причину отклонения Promise. Передать можно любое значение, но на практике лучше передавать Error или свой класс ошибки, чтобы не потерять stack и иметь единый формат обработки.

Что попадет в catch

Значение из reject попадает в обработчик ошибок без преобразования. Если вы передали строку, в catch будет строка. Если передали Error, будет объект ошибки.

Promise.reject(new Error("Failed to load user"))
  .catch((error) => {
    console.log(error.message);
    console.log(error.stack);
  });

Promise.reject("Failed to load user")
  .catch((reason) => {
    console.log(reason); // строка, не Error
  });

Первый вариант лучше для приложения. Его проще обрабатывать одинаково во всех местах: можно прочитать message, отправить ошибку в мониторинг и сохранить стек вызовов. Второй вариант допустим для маленького примера, но в кодовой базе быстро приводит к проверкам вида typeof reason === "string".

Как выбрать, что передавать

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

Как выбрать значение для reject

1Это реальная ошибка выполнения или сетевой сбой?
Отклоняйте Promise через Error или свой класс ошибки.
2Нужно сохранить статус HTTP или код доменной ошибки?
Добавьте поля status, code, details в Error-наследник.
3Это нормальная ветка бизнес-логики?
Верните успешный результат с явным статусом, не используйте reject.
4Операцию отменил пользователь или AbortController?
Отличайте отмену от сбоя и не показывайте ее как аварийную ошибку.

Пример для API-клиента

Плохой вариант: отклонить Promise строкой и потерять статус ответа.

async function loadUser(id) {
  const response = await fetch(`/api/users/${id}`);

  if (!response.ok) {
    return Promise.reject("Request failed");
  }

  return response.json();
}

На практике компонент не поймет, это 401, 404 или 500. В итоге UI может показать общий toast там, где нужно отправить пользователя на логин или показать состояние "пользователь не найден".

Безопаснее нормализовать ошибку.

class HttpError extends Error {
  constructor(message, { status, url }) {
    super(message);
    this.name = "HttpError";
    this.status = status;
    this.url = url;
  }
}

async function loadUser(id) {
  const response = await fetch(`/api/users/${id}`);

  if (!response.ok) {
    throw new HttpError("Failed to load user", {
      status: response.status,
      url: response.url,
    });
  }

  return response.json();
}

Здесь используется throw, но внутри async функции результат будет тем же для вызывающего кода: функция вернет rejected Promise. При этом ошибка сохранит тип, сообщение и статус.

reject и async/await

Когда вы пишете await для Promise, который был отклонен через reject, управление переходит в catch. Поэтому при ответе можно связать Promise-стиль и async/await.

async function renderUser() {
  try {
    const user = await loadUser("42");
    return user.name;
  } catch (error) {
    if (error instanceof HttpError && error.status === 404) {
      return "User not found";
    }

    throw error;
  }
}

Практический вывод для фронтенда: обработчик должен не только вывести ошибку в консоль. Обычно он еще снимает loading, показывает fallback, делает rollback optimistic UI или решает, можно ли повторить запрос.

Отмена запроса и состояние UI

Во фронтенде rejected Promise не всегда значит аварийный сбой. Например, запрос мог быть отменен, потому что пользователь ушел со страницы или быстро ввел новый текст поиска. Если обработать отмену как обычную ошибку, UI покажет ложный toast, перетрет новые данные старым состоянием или оставит кнопку в loading.

Опасный вариант: любой catch переводит экран в ошибку.

async function submit(signal) {
  setLoading(true);

  try {
    await fetch("/api/profile", { signal });
  } catch (error) {
    setError("Не удалось сохранить профиль");
  } finally {
    setLoading(false);
  }
}

Если запрос отменили намеренно, пользователь все равно увидит сообщение об ошибке. Безопаснее отличить отмену от сбоя и оставить catch для реальной проблемы.

async function submit(signal) {
  setLoading(true);

  try {
    await fetch("/api/profile", { signal });
    setError(null);
  } catch (error) {
    if (error?.name === "AbortError") {
      return;
    }

    setError("Не удалось сохранить профиль");
  } finally {
    setLoading(false);
  }
}

На интервью это можно сформулировать так: значение из reject должно помогать UI принять решение. Настоящую ошибку показываем и логируем. Ожидаемую отмену обрабатываем тихо, но состояние загрузки все равно закрываем.

Что важно для UI

Ошибка в Promise почти всегда связана с состоянием интерфейса. Если вы отклонили Promise, но не обработали причину, пользователь может остаться со спиннером, заблокированной кнопкой или пустым экраном.

Безопасный поток
  1. 1Создать Error с понятным message
  2. 2Добавить контекст, если он нужен UI
  3. 3Отклонить Promise или бросить ошибку
  4. 4Обработать в catch или try/catch
  5. 5Снять loading в finally или в явной ветке
  6. 6Показать подходящее состояние без ложного error state
Опасный поток
  1. 1Передать строку без стека
  2. 2Смешать ошибку и обычный результат
  3. 3Не поставить catch
  4. 4Оставить компонент в loading
  5. 5Потерять причину сбоя в логах

Практический вывод

На интервью не ограничивайтесь фразой "reject принимает ошибку". Это звучит неполно, потому что по факту он принимает любое значение. Лучше показать оба уровня ответа: формальный контракт и хорошую практику.

Сильная формулировка:

reject принимает один аргумент, reason отклонения. Тип не ограничен, но я бы передавал Error или свой Error-наследник. Так catch получает нормальную ошибку со stack и контекстом, а UI может обработать разные сбои без догадок по строкам.

Если вас спросят про строку, не спорьте, что она запрещена. Скажите, что она допустима, но хуже для поддержки и наблюдаемости.

Частые ошибки

Где обычно ошибаются

Проверьте формулировки, которые звучат уверенно, но на интервью быстро выдают пробелы.

  1. 1

    Передавать строку вместо Error в рабочем коде

    Строка допустима по спецификации, но у нее нет stack и стабильной структуры. Из-за этого вам придется добавлять проверки на типы в логировании и обработчиках ошибок. В рабочем коде безопаснее передавать new Error("...") или свой класс ошибки.
  2. 2

    Использовать reject для обычных результатов

    Если пользователь закрыл модальное окно или выбрал пустой фильтр, это не всегда ошибка. Когда такие случаи уходят в catch, ваш код начинает показывать лишние toasts и считать нормальное поведение сбоем. Лучше вернуть объект результата, например { status: "cancelled" }.
  3. 3

    Не обрабатывать отклонение в UI

    Rejected Promise без catch часто оставляет кнопку заблокированной, спиннер включенным или форму без сообщения. На интервью скажите не только про синтаксис, но и про состояние интерфейса. В обработчике ошибки нужно снять loading и показать понятную реакцию.
  4. 4

    Терять контекст HTTP-ошибки

    Если сделать reject(new Error("Request failed")) без статуса и тела ответа, UI не отличит авторизацию от ошибки сервера. В API-клиенте лучше сохранить status, url и безопасную часть ответа. Тогда вы сможете по-разному обработать 401, 404 и 500.
  5. 5

    Считать отмененный запрос обычной ошибкой

    При уходе со страницы или новом поисковом запросе старый запрос могут отменить. Если такую отмену показывать как ошибку, пользователь увидит ложный toast или error state. Проверяйте AbortError или свой признак отмены, затем отдельно снимайте loading.

Follow-up

Что могут спросить дальше

Короткие ответы на вопросы, которыми проверяют понимание reject, catch и async/await.

Живые ответы

Видео с похожим вопросом

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

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

Содержание