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

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

Что такое finally в Promise

finally выполняет callback после завершения Promise при любом исходе. В ответе важно показать, что это инструмент для cleanup, а не замена then или catch.

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

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

🐰0
🥚0

Мини-квиз

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

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

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

Что ответить про назначение finally?

Вы объясняете метод Promise.prototype.finally на интервью. Какая формулировка самая точная?

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

Разбор

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

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

Базовая идея

Promise.prototype.finally добавляет действие на момент, когда Promise перешел в конечное состояние. Это состояние может быть fulfilled или rejected. Поэтому короткий ответ такой: finally нужен для кода, который должен выполниться в любом случае.

Здесь легко сказать лишнее. finally не получает аргументы. Внутри callback вы не знаете напрямую, что пришло: данные или ошибка. Поэтому туда не кладут бизнес-логику обработки результата.

Для фронтенда вывод простой. Loader, временная блокировка кнопки, технический cleanup и сброс локального флага могут жить в finally. Показ ошибки, парсинг ответа и обновление данных должны быть в then, catch или в try/catch при async/await.

Что происходит с цепочкой

finally возвращает новый Promise. Если callback завершился без ошибки, новый Promise повторит исходный результат. Успешное значение останется успешным значением. Причина reject останется причиной reject.

Promise.resolve("data")
  .finally(() => "ignored")
  .then((value) => {
    console.log(value); // "data"
  });

В этом примере строка "ignored" не попадет в следующий then. Это частая ловушка: обычный return из finally не заменяет данные.

Но если callback падает, результат меняется:

Promise.resolve("data")
  .finally(() => {
    throw new Error("cleanup failed");
  })
  .catch((error) => {
    console.log(error.message); // "cleanup failed"
  });

Здесь исходное "data" потеряно, потому что ошибка в cleanup стала новой ошибкой цепочки. В реальном коде cleanup лучше делать простым и безопасным.

Как выбрать место для finally

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

Как выбрать обработчик

1Нужно выполнить одно действие и при успехе, и при ошибке?
Поставьте cleanup в finally.
2Нужно показать сообщение об ошибке или восстановить состояние?
Используйте catch или try/catch, не finally.
3Нужно преобразовать успешный результат?
Делайте это в then или после await до блока finally.
4Запросы могут идти параллельно или компонент может размонтироваться?
Добавьте request id, AbortController или другой guard перед setState.

Порядок в цепочке тоже важен. Если написать promise.finally(cleanup).catch(handleError), cleanup выполнится до обработки ошибки. Если написать promise.catch(handleError).finally(cleanup), сначала сработает обработка ошибки, а потом cleanup.

Оба варианта могут быть правильными. Сильный ответ звучит так: я ставлю finally там, где мне нужен общий cleanup относительно остальных обработчиков. И не использую его как скрытый catch.

Где это полезно во фронтенде

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

setLoading(true);

fetch("/api/profile")
  .then((response) => {
    if (!response.ok) {
      throw new Error("Request failed");
    }

    return response.json();
  })
  .then((profile) => {
    setProfile(profile);
  })
  .catch((error) => {
    setError(error);
  })
  .finally(() => {
    setLoading(false);
  });

Этот пример хорош тем, что роли разделены. Проверка response.ok и парсинг живут в обработке результата. Ошибка живет в catch. Общий флаг загрузки снимается в finally.

Но пример безопасен только для одиночного запроса на живом экране. Если пользователь быстро меняет id, фильтр или вкладку, старый запрос может завершиться позже нового и вызвать setLoading(false) не вовремя. Если компонент уже размонтирован, старый ответ может попытаться обновить неактуальное состояние. На практике лучше отменять запрос или проверять, что ответ все еще относится к текущему экрану.

Безопасный поток
  1. 1Запустить запрос и показать loader.
  2. 2В then или try обработать данные и проверить response.ok.
  3. 3В catch показать ошибку или пробросить ее выше.
  4. 4В finally выполнить только общий cleanup.
  5. 5Перед setState проверить, что ответ еще актуален.
Опасный поток
  1. 1Положить обработку ошибки в finally.
  2. 2Вернуть новое значение из finally и ждать, что оно заменит результат.
  3. 3Бросить ошибку в cleanup и скрыть исходную причину сбоя.
  4. 4Снять loader старым запросом во время нового запроса.

Пример с защитой от старого ответа

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

const requestId = useRef(0);

async function loadUser(id) {
  const currentRequest = requestId.current + 1;
  requestId.current = currentRequest;
  setLoading(true);

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

    if (!response.ok) {
      throw new Error("Request failed");
    }

    const user = await response.json();

    if (currentRequest === requestId.current) {
      setUser(user);
    }
  } catch (error) {
    if (currentRequest === requestId.current) {
      setError(error);
    }
  } finally {
    if (currentRequest === requestId.current) {
      setLoading(false);
    }
  }
}

Здесь finally все еще отвечает за cleanup. Но cleanup выполняет setLoading(false) только для последнего актуального запроса. Так UI защищен от старого ответа, который мог бы перезаписать данные или погасить loader для нового запроса.

Пример с отменой в useEffect

Когда запрос привязан к компоненту, одного finally мало. Нужно отменить запрос при смене зависимости или размонтировании. Иначе ответ может прийти уже на другой экран.

useEffect(() => {
  const controller = new AbortController();

  setLoading(true);

  fetch(`/api/users/${id}`, { signal: controller.signal })
    .then((response) => {
      if (!response.ok) {
        throw new Error("Request failed");
      }

      return response.json();
    })
    .then((user) => {
      setUser(user);
    })
    .catch((error) => {
      if (error.name !== "AbortError") {
        setError(error);
      }
    })
    .finally(() => {
      if (!controller.signal.aborted) {
        setLoading(false);
      }
    });

  return () => {
    controller.abort();
  };
}, [id]);

Здесь cleanup эффекта отменяет старый запрос. finally все еще выполняется, но проверка signal.aborted не дает отмененному запросу менять состояние загрузки. Так UI не мигает старым loader и не показывает ошибку от штатной отмены.

Ловушки, которые стоит проговорить

Как finally ведет себя в спорных случаях

return valueignored

Обычный return из finally не заменяет исходные данные. Не пытайтесь так подменять результат запроса.

thrownew reject

Ошибка в finally заменит исходный успех или исходную ошибку. Логирование и cleanup должны быть безопасными.

return rejected Promisenew reject

Цепочка дождется callback и отклонится новой причиной. Так можно случайно скрыть настоящую ошибку API.

без catcherror passes

finally выполнится, но reject пойдет дальше. Нужен catch или try/catch выше по цепочке.

fetch 404fulfilled response

finally выполнится, но сам по себе не поймет HTTP ошибку. Проверяйте response.ok до парсинга данных.

Самая важная ловушка: finally не делает ошибку обработанной. Если цепочка была rejected, после finally она останется rejected, пока вы не обработаете ее в catch или не поймаете через try/catch.

fetch("/api/profile")
  .finally(() => {
    setLoading(false);
  });

// Плохо: при сетевой ошибке нет catch, ошибка уйдет как необработанный reject.

Что сломается в UI: loader исчезнет, но пользователь не увидит понятный error state. Ошибка может попасть только в консоль или глобальный обработчик. А экран останется пустым или со старыми данными.

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

async/await и finally

С async/await та же идея часто выглядит через обычный try/catch/finally. Для ответа на интервью полезно связать эти две формы.

async function saveForm(values) {
  setSubmitting(true);

  try {
    const response = await fetch("/api/form", {
      method: "POST",
      body: JSON.stringify(values),
    });

    if (!response.ok) {
      throw new Error("Save failed");
    }

    return await response.json();
  } catch (error) {
    showError(error);
    throw error;
  } finally {
    setSubmitting(false);
  }
}

Здесь finally не обязан знать, сохранилась форма или нет. Он только возвращает UI из состояния отправки. Если в finally бросить новую ошибку, она так же заменит исходную ошибку или успешный результат функции.

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

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

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

  1. 1

    Путать cleanup и обработку ошибки

    finally не получает объект ошибки и не должен решать, какой текст показать пользователю. Если вы кладете туда обработку ошибки, ответ звучит так, будто вы не различаете роли catch и finally. Надежнее сказать: ошибку обрабатываю в catch, а в finally снимаю общий побочный эффект.
  2. 2

    Считать, что finally вообще не влияет на цепочку

    Возвращенное обычное значение действительно игнорируется. Но ошибка или rejected Promise внутри finally заменят исходный результат. Поэтому cleanup не должен бросать случайные ошибки. Иначе вы потеряете настоящую причину сбоя.
  3. 3

    Снимать loader без учета гонок

    setLoading(false) в finally выглядит правильно, пока запрос один. Если пользователь быстро меняет фильтр или id, старый запрос может завершиться позже и погасить loader для нового запроса. Нужен AbortController, request id или проверка актуальности перед обновлением UI.
  4. 4

    Забывать cleanup эффекта в React

    finally сработает после завершения запроса, но он не отменит запрос при размонтировании компонента. Без cleanup старый ответ может обновить состояние уже неактуального экрана. В useEffect безопаснее использовать AbortController и игнорировать AbortError как штатную отмену.
  5. 5

    Думать, что finally ловит HTTP ошибки fetch

    fetch не делает reject на каждый плохой HTTP статус. Ответ с 404 или 500 нужно проверять через response.ok. finally только выполнит cleanup после завершения цепочки.
  6. 6

    Ставить finally в цепочку без понимания порядка

    promise.finally(cleanup).catch(handle) и promise.catch(handle).finally(cleanup) отличаются порядком действий и тем, что идет дальше. На интервью лучше прямо сказать, где именно вам нужен cleanup: до обработки ошибки или после нее.

Follow-up

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

Короткие ответы на вопросы, которыми проверяют понимание Promise.finally и его поведения в UI коде.

Живые ответы

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

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

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

Содержание