Kodokon kodokon.com

Асинхронность вглубь: промисы, async/await, обработка ошибок

Используй событийный цикл, правильно распараллеливай промисы и сделай обработку асинхронных ошибок надёжной.

11 мин · 3 вопросов

Открыть этот урок в Kodokon

Модель выполнения опирается на очередь макрозадач (setTimeout, ввод-вывод) и очередь микрозадач (.then, await, queueMicrotask). После каждой задачи движок полностью опустошает очередь микрозадач, прежде чем идти дальше. Прямое следствие: разрешённый промис всегда выполнится раньше setTimeout(fn, 0), а слишком длинная цепочка микрозадач может подвесить отрисовку так же надёжно, как синхронный цикл.

JAVASCRIPT
console.log("start");
setTimeout(() => console.log("macrotask"), 0);
Promise.resolve().then(() => console.log("microtask"));
console.log("end");
// start, end, microtask, macrotask
Микрозадачи выполняются раньше любой ожидающей макрозадачи.

async/await - это синтаксический сахар поверх промисов, но он прячет ловушку производительности: каждый await внутри цикла сериализует операции. Если запросы независимы, сначала запусти их все, а потом дождись пачку через Promise.all. Для трёх запросов по 100 мс последовательная версия стоит 300 мс, параллельная - 100 мс.

JAVASCRIPT
const fetchUser = (id) => new Promise((res) => {
  setTimeout(() => res({ id }), 100);
});
async function loadSequential(ids) {
  const users = [];
  for (const id of ids) {
    users.push(await fetchUser(id)); // ~100 ms each
  }
  return users;
}
async function loadParallel(ids) {
  return Promise.all(ids.map(fetchUser)); // ~100 ms total
}
Тот же результат, задержка поделена на количество вызовов.

Выбирай комбинатор под нужный тебе контракт отказа: Promise.all отклоняется на первой ошибке (всё или ничего), allSettled дожидается всех и описывает каждый исход, race берёт первое завершение (успех или ошибку - удобно для таймаута), any берёт первый успех и отклоняется только если провалилось всё. Для дашборда, который обязан показать частичные результаты, allSettled почти всегда правильный выбор.

JAVASCRIPT
const tasks = [
  Promise.resolve("profile loaded"),
  Promise.reject(new Error("network unavailable")),
];
const results = await Promise.allSettled(tasks);
for (const r of results) {
  if (r.status === "fulfilled") {
    console.log("success:", r.value);
  } else {
    console.error("failure:", r.reason.message);
  }
}
Описан каждый исход; ни одна ошибка не прячет остальные.

Проверка знаний

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

  1. В каком порядке выполнятся синхронный console.log, колбэк Promise.resolve().then и колбэк setTimeout(fn, 0)?
    • синхронный, setTimeout, then
    • синхронный, then, setTimeout
    • then, синхронный, setTimeout
  2. Ты отправляешь десять независимых запросов. Какой подход минимизирует общую задержку?
    • По одному await на запрос внутри цикла for...of
    • Создать все промисы, затем дождаться Promise.all
    • Сцепить вызовы .then один за другим
    • Использовать setTimeout, чтобы разнести вызовы по времени
  3. Почему try { doAsync(); } catch (e) {} без await не ловит отклонение от doAsync?
    • Потому что catch не работает внутри async-функций
    • Потому что отклонение происходит после выхода из блока try, так как промис никто не дождался
    • Потому что промисы можно ловить только через .catch