Коротка відповідь. Promise – це об'єкт-обіцянка результату, який перебуває в одному з трьох станів: pending, fulfilled або rejected. async/await – синтаксис поверх промісів, не окремий механізм. На співбесідах найчастіше валяться не на визначеннях, а на трьох речах: паралельність (Promise.all проти послідовних await), обробка помилок (де саме спрацює catch) і поведінка проміса в комбінації з циклами й map. Розберімо кожну.
Стани проміса за одну хвилину
Проміс створюється в стані pending і рівно один раз переходить у fulfilled (зі значенням) або rejected (з причиною). Повторно змінити стан неможливо – другий виклик resolve чи reject ігнорується. await розгортає fulfilled-значення або кидає rejected-причину як виняток. Базу добре пояснює гайд MDN про використання промісів; тут зосередимось на практиці.
Послідовно чи паралельно: головне питання співбесіди
Класична помилка, яку інтерв'юери шукають спеціально:
// Послідовно: друга «запит» чекає першу. Разом ~2 сек
const user = await fetchUser(id); // ~1 сек
const orders = await fetchOrders(id); // ~1 секЯкщо запити незалежні – запускай їх одночасно:
// Паралельно: разом ~1 сек
const [user, orders] = await Promise.all([
fetchUser(id),
fetchOrders(id),
]);Коли що використовувати:
Promise.all– потрібні всі результати, і будь-яка помилка робить решту безглуздою. Падає з першим reject.Promise.allSettled– потрібні всі результати, але часткова відмова прийнятна (наприклад, віджети дашборда). Ніколи не reject-иться, повертає масив{ status, value | reason }.Promise.race– потрібен перший результат, байдуже який: типово таймаут проти запиту.
const results = await Promise.allSettled([fetchUser(id), fetchOrders(id)]);
for (const result of results) {
if (result.status === 'fulfilled') {
render(result.value);
} else {
showWidgetError(result.reason);
}
}На співбесіді сильна відповідь – не «завжди Promise.all», а «залежить від того, чи роблять часткові дані інтерфейс корисним».
Пастки try/catch
Пастка 1 – повернення без await. catch нижче не спрацює, бо проміс покидає функцію ще pending:
async function loadUser(id: string) {
try {
return fetchUser(id); // немає await!
} catch (error) {
log(error); // сюди НЕ потрапимо при reject
throw error;
}
}Правильно: return await fetchUser(id); – тоді reject перехопиться в цьому try/catch.
Пастка 2 – async у map. array.map(async ...) повертає масив промісів, а не результатів:
const ids = ['1', '2', '3'];
// Помилка: users – це Promise<User>[]
const users = ids.map(async (id) => fetchUser(id));
// Правильно: дочекатися всіх
const loaded = await Promise.all(ids.map((id) => fetchUser(id)));Бонусне запитання інтерв'юера: «а якщо треба строго по черзі?» Відповідь – цикл for...of з await усередині, і варто вміти пояснити, чому це рідко потрібно (повільніше) і коли таки потрібно (rate limit, залежність наступного запиту від попереднього).
Пастка 3 – проміс стартує при створенні. await не «запускає» проміс – він уже виконується з моменту виклику функції. Тому такий код таки паралельний:
const userPromise = fetchUser(id); // старт одразу
const ordersPromise = fetchOrders(id); // старт одразу
const user = await userPromise;
const orders = await ordersPromise;Строго послідовно: коли цикл кращий за Promise.all
Інтерв'юери люблять продовження: «а тепер зроби так, щоб запити йшли по одному». Відповідь – звичайний цикл з await:
async function loadInOrder(ids: string[]): Promise<User[]> {
const users: User[] = [];
for (const id of ids) {
users.push(await fetchUser(id)); // наступний стартує після попереднього
}
return users;
}Важливо вміти назвати, коли це виправдано: зовнішнє API з лімітом запитів, кожен наступний запит залежить від попереднього результату, або запис у ресурс, що не терпить конкурентних змін. У всіх інших випадках послідовний цикл – просто повільніша версія Promise.all, і сказати це вголос теж частина правильної відповіді.
Компроміс між «усе одразу» і «по одному» – обмежена конкурентність: розбити список на порції.
async function loadInBatches(ids: string[], batchSize = 5): Promise<User[]> {
const result: User[] = [];
for (let i = 0; i < ids.length; i += batchSize) {
const batch = ids.slice(i, i + batchSize);
result.push(...(await Promise.all(batch.map(fetchUser))));
}
return result;
}Це вже рівень Middle: задача та сама, але відповідь враховує реальні обмеження бекенда.
Повторні спроби з паузою
Ще один практичний патерн, який просять написати «з голови», – retry з експоненційною затримкою:
async function withRetry<T>(
action: () => Promise<T>,
attempts = 3,
baseDelayMs = 200
): Promise<T> {
let lastError: unknown;
for (let attempt = 0; attempt < attempts; attempt++) {
try {
return await action();
} catch (error) {
lastError = error;
const delay = baseDelayMs * 2 ** attempt;
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
throw lastError;
}
const user = await withRetry(() => fetchUser('1'));Тут зібрано одразу кілька тем зі співбесіди: generic-типізація, «промісифікація» setTimeout і акуратне прокидання останньої помилки після вичерпання спроб. Якщо додаси уточнення «повторювати варто лише мережеві/5xx-помилки, а не 400-ті» – відповідь стане production-рівня.
Скасування: AbortController
Промісам не можна «наказати зупинитись», але запит можна скасувати кооперативно:
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000);
try {
const response = await fetch('/api/search?q=react', {
signal: controller.signal,
});
return await response.json();
} catch (error) {
if (error instanceof DOMException && error.name === 'AbortError') {
return null; // скасовано таймаутом – не показуємо помилку
}
throw error;
} finally {
clearTimeout(timeoutId);
}Типовий фронтенд-кейс: скасовувати попередній запит пошуку, коли користувач продовжив вводити текст. Це часта задача на React live coding.
Три задачі «що виведе код»
Задача 1.
console.log('a');
Promise.resolve().then(() => console.log('b'));
console.log('c');
// a, c, bКолбек .then – мікрозадача: виконається після синхронного коду. Чому саме так – детально в статті про Event Loop.
Задача 2.
async function run() {
try {
await Promise.reject(new Error('boom'));
} catch (e) {
console.log('caught:', e.message);
}
}
run();
// caught: boomawait перетворює reject на звичайний виняток – тому try/catch працює.
Задача 3.
const p = new Promise((resolve) => {
resolve(1);
resolve(2);
});
p.then((v) => console.log(v));
// 1Стан фіксується першим resolve; другий ігнорується без помилки.
Як тренуватись
Візьми будь-яку функцію з запитами зі свого пет-проєкту і постав собі три питання: що тут можна розпаралелити; де саме перехопиться помилка; що станеться, якщо компонент розмонтується до відповіді. Потім перевір відповіді кодом. Ширший план підготовки – у гайді з JavaScript-співбесіди і в добірці live-coding задач.