Асинхронность JavaScript позволяет не блокировать выполнение программы, пока ожидаются ввод-вывод, таймеры или сетевые запросы. Для управления результатами используют Promise, синтаксис async/await и очередь задач. Чтобы писать предсказуемый код, важно понимать, когда выполняются обработчики Promise, как обрабатывать ошибки и какой метод группировки подходит для параллельных операций.
Карта асинхронного выполнения в JavaScript
- Исполнитель Promise запускается сразу, а обработчики
then,catchиfinallyвыполняются позднее. asyncвсегда возвращает Promise;awaitприостанавливает выполнение текущей async-функции, но не блокирует весь поток JavaScript.- После синхронного кода движок выполняет ожидающие микрозадачи, затем может перейти к следующей макрозадаче.
Promise.allподходит, когда нужны все результаты;allSettled- когда важно получить итоги всех операций;race- когда нужен первый завершившийся результат.- Ошибки нужно обрабатывать там, где их можно корректно передать вызывающему коду или показать пользователю.
Как Promise проходит от создания до завершения
Promise удобен, когда результат операции появится позже, а вызывающему коду нужно зарегистрировать обработчики успеха и ошибки. Он также полезен для объединения уже существующих асинхронных операций. Не создавайте Promise вручную, если используемый API уже возвращает его: лишняя обёртка усложняет код и может скрыть ошибки.
const result = fetch("/api/data")
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
})
.catch((error) => {
console.error("Не удалось получить данные", error);
throw error;
});
Конструктор Promise нужен, например, чтобы превратить callback-API в Promise. Его исполнитель вызывается синхронно; вызовы resolve и reject переводят Promise в завершённое состояние. Обработчики, добавленные через then или catch, выполняются асинхронно.
const result = new Promise((resolve, reject) => {
legacyOperation((error, value) => {
if (error) reject(error);
else resolve(value);
});
});
В таких JavaScript Promise примерах важно не проглатывать ошибку: если обработчик поймал её, но не вернул запасной результат и не выбросил исключение дальше, следующая часть цепочки может завершиться успешно.
Когда async/await упрощает цепочку, а когда скрывает ошибки
Для async/await нужен современный JavaScript-движок или сборка, которая преобразует синтаксис для целевых сред. В браузере также понадобится доступ к API, которое возвращает Promise; для тестирования подойдут обычные тесты и инструменты разработчика. Сам синтаксис не отменяет необходимости проверять HTTP-статус и обрабатывать отклонённые Promise.
Используйте try/catch, если можете обработать ошибку на месте. Если исправить её нельзя, обычно лучше передать исключение дальше, а не возвращать неопределённое значение.
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("Загрузка не удалась", error);
throw error;
}
}
Последовательные await подходят, когда следующая операция зависит от результата предыдущей. Если зависимости нет, ожидайте операции параллельно с помощью метода группировки Promise. Так async await JavaScript остаётся читаемым и не создаёт лишних задержек.
Микрозадачи и макрозадачи: как читается очередь событий
Чтобы разобраться, почему сообщения появляются в определённом порядке, проследите путь от синхронного кода к очередям. Это практический способ понять очередь задач JavaScript и проверить собственные предположения в консоли.
- Выполните синхронный код. Сначала выполняется текущий стек вызовов: в том числе исполнитель конструктора Promise и код до первого
await. - Зарегистрируйте отложенную работу. Обработчики Promise ставятся в очередь микрозадач, а callback таймера становится доступен для выполнения как отдельная задача после срабатывания таймера.
- Дождитесь освобождения стека. Пока выполняется синхронный код, обработчики Promise и callback таймера не прерывают его.
- Выполните микрозадачи. После текущего стека движок выполняет ожидающие обработчики Promise. Если они добавляют новые микрозадачи, те также выполняются до перехода к следующей задаче.
- Перейдите к следующей задаче. Когда микрозадачи исчерпаны, среда может выполнить callback таймера или другую готовую задачу. Точный момент зависит от среды и состояния очередей.
console.log("синхронно");
setTimeout(() => console.log("таймер"), 0);
Promise.resolve().then(() => console.log("обработчик Promise"));
console.log("конец синхронного кода");
В этом примере синхронные сообщения появятся раньше обработчика Promise, а обработчик Promise - раньше callback таймера. Значение задержки таймера не означает, что callback выполнится немедленно.
Быстрый режим
- Найдите весь код, который выполняется синхронно.
- Отметьте обработчики Promise и продолжения после
awaitкак микрозадачи. - Отметьте таймеры и другие callbacks среды как отдельные задачи.
- Расположите микрозадачи после текущего синхронного кода и до следующей задачи.
Promise.all, allSettled и race: выберите способ параллельного запуска
Эти методы принимают набор значений и Promise, но задают разное поведение при завершении. Выбирайте метод по тому, что нужно вызывающему коду: все успешные результаты, итог каждого задания или первый завершившийся результат.
const [profile, settings] = await Promise.all([
loadProfile(),
loadSettings(),
]);
Если один Promise в Promise.all отклоняется, общий Promise отклоняется с этой ошибкой. Это не отменяет автоматически остальные операции. Promise.allSettled дожидается завершения всех входящих Promise и возвращает статус каждого. Promise.race завершается по первому завершившемуся Promise - успешному или отклонённому; сам по себе он также не отменяет проигравшие операции.
- Убедитесь, что операции действительно не зависят друг от друга, прежде чем запускать их параллельно.
- Выберите
Promise.all, если нужны все результаты и любой отказ должен считаться общим отказом. - Выберите
Promise.allSettled, если нужно обработать результат каждой операции независимо от её исхода. - Выберите
Promise.race, если важен первый завершившийся результат, включая отклонение. - Для ограничения времени ожидания не рассчитывайте, что
raceотменит медленный запрос: организуйте отмену отдельно, если API её поддерживает. - Обработайте отклонение общего Promise через
try/catchилиcatch.
Типичные ловушки: потерянные ошибки, лишнее ожидание и гонки
- Забытый return в цепочке. Без
returnследующий обработчик не получит результат вложенного Promise. - Неожидаемый Promise внутри async-функции. Если не поставить
awaitи не вернуть Promise, ошибка может произойти уже после завершения функции. - Пустой catch. Обработчик, который подавляет ошибку без восстановления или передачи дальше, затрудняет диагностику и может оставить программу без результата.
- Последовательный запуск независимых операций. Несколько последовательных
awaitувеличивают общее ожидание, когда результат предыдущего вызова не нужен следующему. - Ожидание только первого Promise в списке. Один
awaitне собирает результаты всех независимых операций; для этого используйте подходящий метод группировки. - Ожидание, что race отменит остальные операции.
Promise.raceопределяет итог для ожидающего кода, но не прекращает работу остальных Promise. - Гонка при записи общего состояния. Если несколько операций завершаются в непредсказуемом порядке и меняют одни данные, задайте правило приоритета или защитите обновление.
- Путаница между HTTP-ошибкой и отклонением fetch. Проверьте
response.ok: получение ответа со статусом ошибки само по себе обычно не отклоняет Promise отfetch.
Таблица выбора: Promise, async/await и методы группировки
Выбирайте синтаксис и метод группировки по структуре зависимостей и ожидаемому поведению при ошибке.
| Подход | Когда уместен | Что учесть |
|---|---|---|
Цепочка then/catch |
Короткое преобразование результата или последовательная обработка Promise | Возвращайте Promise и значения, чтобы цепочка сохраняла порядок и ошибки |
async/await |
Несколько последовательных шагов с явной обработкой ошибок | Для параллельных независимых операций сначала создайте все Promise, затем сгруппируйте их |
Promise.all |
Нужны все успешные результаты независимых операций | Общий результат отклоняется при отказе любого входящего Promise |
Promise.allSettled или Promise.race |
allSettled нужен для итогов всех операций; race - для первого завершения |
Ни один из этих методов автоматически не отменяет остальные операции |
Разбор спорных случаев асинхронного кода
Исполнитель Promise выполняется сразу?
Да. Функция, переданная в new Promise, вызывается синхронно; обработчики, зарегистрированные через then или catch, выполняются позднее.
Приостанавливает ли await весь JavaScript?
Нет. await приостанавливает только продолжение текущей async-функции, а среда может выполнять другой код.
Нужно ли проверять response.ok после fetch?
Да. Ответ с HTTP-статусом ошибки обычно не отклоняет Promise от fetch, поэтому статус проверяют отдельно.
Отменяет ли Promise.race проигравшие операции?
Нет. Метод определяет результат общего Promise, но остальные операции продолжают выполняться, если их не отменить отдельным поддерживаемым механизмом.
Как обработать ошибки всех операций, не прекращая сбор результатов?
Используйте Promise.allSettled, затем проверьте статус каждого результата и отдельно решите, что делать с отклонёнными операциями.
Можно ли объединить await и Promise.all?
Да. Обычно создают независимые Promise и ожидают массив результатов через await Promise.all([...]).
Автор: Сергей Павлов.
