Асинхронность: Promise, async/await
JavaScript — однопоточный язык, но большая часть реальных задач требует асинхронности: сетевые запросы, таймеры, работа с файлами, события пользователя. Современный способ работать с асинхронностью — Promise и синтаксический сахар async/await. Понимание их поведения, приоритетов в Event Loop и обработки ошибок — обязательно для собеседований и для написания стабильного кода.
Содержание
- Зачем нужна асинхронность
- Promise: состояния и жизненный цикл
- Promise и Event Loop
- Методы Promise: all, race, allSettled, any
- async/await
- Последовательное и параллельное выполнение
- Обработка ошибок
- Отмена асинхронных операций: AbortController
- Ключевые тезисы для интервью
- Заключение
- Полезные ссылки
Зачем нужна асинхронность
В синхронном коде каждая операция блокирует выполнение следующей. Если запрос к серверу занимает секунду, весь остальной код ждёт:
const data = fetchDataSync(); // блокирует на 1 секунду
console.log('Done');
Асинхронность позволяет начать операцию, передать callback или Promise, и продолжить выполнять другой код. Когда операция завершится, движок вызовет обработчик.
До Promise асинхронность реализовывалась через вложенные callback’и:
getData((err, data) => {
if (err) { /* ... */ }
processData(data, (err, result) => {
if (err) { /* ... */ }
saveResult(result, (err) => {
// callback hell
});
});
});
Promise и async/await заменили этот подход плоским, читаемым кодом.
Promise: состояния и жизненный цикл
Promise — это объект, представляющий результат асинхронной операции. У него три состояния:
- pending — операция ещё не завершена.
- fulfilled — операция успешно завершена, есть результат.
- rejected — операция завершилась с ошибкой.
Состояние может измениться только один раз: из pending в fulfilled или из pending в rejected. После этого Promise становится settled.
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Done');
}, 1000);
});
promise
.then((value) => console.log(value)) // Done
.catch((error) => console.error(error))
.finally(() => console.log('Finished'));
Методы Promise
.then(onFulfilled, onRejected)— обработка успеха или ошибки..catch(onRejected)— обработка ошибки (синоним.then(null, onRejected))..finally(onFinally)— выполняется в любом случае, не получает аргументов.
fetch('/api/user')
.then((response) => response.json())
.then((user) => console.log(user))
.catch((error) => console.error('Request failed:', error))
.finally(() => console.log('Cleanup'));
Цепочки then
Каждый .then возвращает новый Promise. Если callback возвращает значение, следующий .then получит его. Если возвращается Promise, цепочка дождётся его выполнения.
Promise.resolve(1)
.then((x) => x + 1) // 2
.then((x) => Promise.resolve(x * 2)) // 4
.then((x) => console.log(x)); // 4
Promise и Event Loop
Promise используют микрозадачи (microtasks). Callback из .then / .catch / .finally попадает в микрозадачную очередь, а не в очередь макрозадач.
console.log('Start');
setTimeout(() => console.log('Timeout'), 0);
Promise.resolve().then(() => console.log('Promise'));
console.log('End');
// Start
// End
// Promise
// Timeout
Почему Promise выполняется раньше setTimeout? Потому что после синхронного кода Event Loop опустошает все микрозадачи, и только потом берёт следующую макрозадачу.
Это важно для async/await: код после await тоже становится микрозадачей.
async function example() {
console.log('Before await');
await Promise.resolve();
console.log('After await'); // микрозадача
}
example();
console.log('Sync');
// Before await
// Sync
// After await
Методы Promise: all, race, allSettled, any
Promise.all
Ожидает выполнения всех Promise. Возвращает массив результатов в том же порядке. Если хотя бы один Promise отклонён — весь Promise.all отклоняется.
const [users, posts] = await Promise.all([
fetch('/api/users').then(r => r.json()),
fetch('/api/posts').then(r => r.json()),
]);
Promise.race
Возвращает результат первого завершившегося Promise — успешного или отклонённого.
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), 5000)
);
const data = fetch('/api/data');
const result = await Promise.race([data, timeout]);
Promise.allSettled
Ожидает всех, но никогда не отклоняется. Возвращает массив объектов:
const results = await Promise.allSettled([
Promise.resolve(1),
Promise.reject('error'),
]);
// [
// { status: 'fulfilled', value: 1 },
// { status: 'rejected', reason: 'error' }
// ]
Promise.any
Возвращает результат первого успешно выполнившегося Promise. Если все отклонены — возвращает AggregateError.
try {
const fastest = await Promise.any([
fetch('/mirror-1'),
fetch('/mirror-2'),
fetch('/mirror-3'),
]);
} catch (e) {
console.log('All failed:', e.errors);
}
Сравнение
| Метод | Успех | Ошибка | Когда использовать |
|---|---|---|---|
Promise.all | Все выполнились | Первый отказ | Параллельные зависимые запросы |
Promise.race | Первый результат | Первый отказ | Таймауты |
Promise.allSettled | Всегда | Никогда | Нужно узнать результат каждого |
Promise.any | Первый успех | Все отказали | Зеркала, fallback’ы |
async/await
async/await — синтаксический сахар над Promise. Функция, объявленная с async, всегда возвращает Promise.
async function getUser() {
return { id: 1, name: 'Alice' };
}
getUser().then((user) => console.log(user));
await приостанавливает выполнение функции до завершения Promise. Он может использоваться только внутри async-функции (или на верхнем уровне модулей ES).
async function loadUser(id) {
const response = await fetch(`/api/users/${id}`);
const user = await response.json();
return user;
}
Важно: await не блокирует весь Event Loop, только текущую async-функцию. Остальной код продолжает выполняться.
Последовательное и параллельное выполнение
Последовательное
async function sequential() {
const user = await fetchUser();
const posts = await fetchPosts(user.id);
return posts;
}
Запросы выполняются один за другим. Общее время — сумма задержек.
Параллельное
async function parallel() {
const [user, posts] = await Promise.all([
fetchUser(),
fetchPosts(),
]);
return { user, posts };
}
Запросы начинаются одновременно. Общее время — максимальная задержка.
Распространённая ошибка
async function wrong() {
const user = await fetchUser();
const posts = await fetchPosts();
// user и posts не зависят друг от друга, но выполняются последовательно
}
Если запросы независимы, используйте Promise.all.
Обработка ошибок
try/catch с await
async function loadData() {
try {
const response = await fetch('/api/data');
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('Failed to load data:', error.message);
return null;
}
}
.catch() для Promise
fetch('/api/data')
.then((response) => response.json())
.catch((error) => console.error(error));
Unhandled rejection
Если Promise отклонён, но нет обработчика ошибки, возникает unhandledrejection:
Promise.reject(new Error('Oops'));
// Uncaught (in promise) Error: Oops
Можно отловить глобально:
window.addEventListener('unhandledrejection', (event) => {
console.error('Unhandled rejection:', event.reason);
});
Важно: если ошибка обработана через .catch или try/catch, событие не возникает.
Обработка ошибок в Promise.all
try {
const [users, posts] = await Promise.all([
fetchUsers(),
fetchPosts(),
]);
} catch (error) {
// если отказал хотя бы один запрос
console.error(error);
}
Если нужно узнать, какой именно запрос упал, используйте Promise.allSettled.
Отмена асинхронных операций: AbortController
AbortController позволяет отменять асинхронные операции, чаще всего — fetch.
const controller = new AbortController();
fetch('/api/data', { signal: controller.signal })
.then((response) => response.json())
.catch((error) => {
if (error.name === 'AbortError') {
console.log('Request was aborted');
} else {
console.error('Request failed:', error);
}
});
// Отмена через 5 секунд
setTimeout(() => controller.abort(), 5000);
С async/await
async function loadUser(id, signal) {
const response = await fetch(`/api/users/${id}`, { signal });
return response.json();
}
const controller = new AbortController();
loadUser(1, controller.signal);
// При размонтировании компонента или смене запроса:
controller.abort();
React-пример
useEffect(() => {
const controller = new AbortController();
fetch(`/api/search?q=${query}`, { signal: controller.signal })
.then((response) => response.json())
.then(setResults)
.catch((error) => {
if (error.name !== 'AbortError') {
setError(error);
}
});
return () => controller.abort();
}, [query]);
AbortController помогает избежать состояний-гонок (race conditions), когда старый запрос приходит позже нового.
Ключевые тезисы для интервью
Promise.allожидает все промисы и отклоняется при первом отказе;Promise.raceвозвращает результат первого завершившегося промиса.Promise.allSettledнужен, когда важно получить результат каждого промиса независимо от успеха или отказа.async/await— синтаксический сахар над Promise; код послеawaitвыполняется как микрозадача.- Ошибки в
async/awaitобрабатываются черезtry/catch, в цепочках Promise — через.catch(). - Необработанный отказ промиса вызывает событие
unhandledrejection; его можно отловить глобально. AbortControllerпозволяет отменитьfetchи другие асинхронные операции, передаваяsignalв запрос.- Независимые запросы лучше выполнять параллельно через
Promise.all, зависимые — последовательно черезawait. Promise.thenвыполняется раньшеsetTimeout(..., 0), потому что микрозадачи имеют приоритет над макрозадачами в Event Loop.
Заключение
Promise и async/await — основа асинхронного программирования в JavaScript. Понимание состояний промиса, приоритета микрозадач над макрозадачами и методов комбинирования (all, race, allSettled, any) позволяет писать эффективный и предсказуемый код. Обработка ошибок через try/catch или .catch() и отмена операций через AbortController — обязательные навыки для стабильной работы с асинхронностью. Потренируйтесь переписывать вложенные callback’и на async/await и выбирать между последовательным и параллельным выполнением запросов.