Интервью-вопрос
Что такое finally в Promise
finally выполняет callback после завершения Promise при любом исходе. В ответе важно показать, что это инструмент для cleanup, а не замена then или catch.
- Добавлен
- Редакция
Подготовьте короткий ответ и пару деталей на случай уточняющих вопросов.
Мини-квиз
Проверка перед разбором
Несколько быстрых вопросов перед разбором. Так проще поймать места, которые только кажутся понятными.
Вопрос 1 из 50 правильно
Разбор
Разобраться, а не зазубрить
Дальше разбираем суть, типичные уточнения и места, где легко сказать лишнее или перепутать термины.
Базовая идея
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. Вопрос в том, какую ответственность вы в него кладете.
Как выбрать обработчик
Поставьте cleanup в finally.Используйте catch или try/catch, не finally.Делайте это в then или после await до блока finally.Добавьте 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Запустить запрос и показать loader.
- 2В then или try обработать данные и проверить response.ok.
- 3В catch показать ошибку или пробросить ее выше.
- 4В finally выполнить только общий cleanup.
- 5Перед setState проверить, что ответ еще актуален.
- 1Положить обработку ошибки в finally.
- 2Вернуть новое значение из finally и ждать, что оно заменит результат.
- 3Бросить ошибку в cleanup и скрыть исходную причину сбоя.
- 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 ведет себя в спорных случаях
ignoredОбычный return из finally не заменяет исходные данные. Не пытайтесь так подменять результат запроса.
new rejectОшибка в finally заменит исходный успех или исходную ошибку. Логирование и cleanup должны быть безопасными.
new rejectЦепочка дождется callback и отклонится новой причиной. Так можно случайно скрыть настоящую ошибку API.
error passesfinally выполнится, но reject пойдет дальше. Нужен catch или try/catch выше по цепочке.
fulfilled responsefinally выполнится, но сам по себе не поймет 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
Путать cleanup и обработку ошибки
finallyне получает объект ошибки и не должен решать, какой текст показать пользователю. Если вы кладете туда обработку ошибки, ответ звучит так, будто вы не различаете ролиcatchиfinally. Надежнее сказать: ошибку обрабатываю вcatch, а вfinallyснимаю общий побочный эффект. - 2
Считать, что finally вообще не влияет на цепочку
Возвращенное обычное значение действительно игнорируется. Но ошибка или rejected Promise внутриfinallyзаменят исходный результат. Поэтому cleanup не должен бросать случайные ошибки. Иначе вы потеряете настоящую причину сбоя. - 3
Снимать loader без учета гонок
setLoading(false)вfinallyвыглядит правильно, пока запрос один. Если пользователь быстро меняет фильтр или id, старый запрос может завершиться позже и погасить loader для нового запроса. НуженAbortController, request id или проверка актуальности перед обновлением UI. - 4
Забывать cleanup эффекта в React
finallyсработает после завершения запроса, но он не отменит запрос при размонтировании компонента. Без cleanup старый ответ может обновить состояние уже неактуального экрана. ВuseEffectбезопаснее использоватьAbortControllerи игнорироватьAbortErrorкак штатную отмену. - 5
Думать, что finally ловит HTTP ошибки fetch
fetchне делает reject на каждый плохой HTTP статус. Ответ с404или500нужно проверять черезresponse.ok.finallyтолько выполнит cleanup после завершения цепочки. - 6
Ставить finally в цепочку без понимания порядка
promise.finally(cleanup).catch(handle)иpromise.catch(handle).finally(cleanup)отличаются порядком действий и тем, что идет дальше. На интервью лучше прямо сказать, где именно вам нужен cleanup: до обработки ошибки или после нее.
Follow-up
Что могут спросить дальше
Короткие ответы на вопросы, которыми проверяют понимание Promise.finally и его поведения в UI коде.
Живые ответы
Видео с похожим вопросом
Если найдем публичные интервью с таким вопросом, добавим их сюда. Их удобно смотреть после теории, чтобы свериться с живыми ответами.
Пока видео нет. Когда появятся подходящие публичные интервью, добавим их в этот блок, чтобы можно было сравнить разбор с тем, как отвечают реальные кандидаты.
Что сработает первое, setInterval или Promise 😎
Если setInterval и Promise запланированы в одном синхронном участке кода, обработчик Promise выполнится раньше. На странице разбираем очереди задач, микрозадачи и практический риск для таймеров во фронтенде.
Что такое Promise
Разбор вопроса «Что такое Promise» для Frontend Developer: что проверяет интервьюер, ключевые тезисы, практические примеры и частые ошибки.