Программирование с ИИ
ENRU
9.2

Асинхронность в JavaScript

промисы, await и четыре места, где ИИ ошибается

Что это

Асинхронность в JavaScript устроена так же, как в Python (статья 2.10): async, await, ожидание вместо простоя. Но есть своя терминология и свои ловушки, и все четыре ниже ИИ воспроизводит регулярно.

Здесь же — прямая параллель с главой 6: Promise.all и Promise.allSettled соотносятся ровно так же, как gather и gather(return_exceptions=True).

Как выглядит в коде

const fetchCity = (city, ms = 100) =>
  new Promise((resolve, reject) =>
    setTimeout(() => city === "Bergen"
      ? reject(new Error("сервер вернул 500"))
      : resolve(`${city}: +4`), ms));

Промис (promise) — обещание результата, который появится позже. У него три состояния: ожидает, выполнен, отклонён. resolve — успех, reject — ошибка.

Писать new Promise вручную почти никогда не нужно: промисы возвращают fetch и библиотеки. Но знать, что это такое, надо — слово встречается постоянно.

1. Promise.all роняет всё целиком

await Promise.all([fetchCity("Oslo"), fetchCity("Bergen"), fetchCity("Tromsø")]);
измерено
упало целиком: сервер вернул 500

Запросы к Осло и Тромсё прошли успешно — их результаты потеряны. Ровно то же, что asyncio.gather без защиты в Python.

2. Promise.allSettled сохраняет успехи

const r = await Promise.allSettled([...]);
for (const x of r)
  console.log(x.status === "fulfilled" ? `ок: ${x.value}` : `ошибка: ${x.reason.message}`);
измерено
ок: Oslo: +4
ошибка: сервер вернул 500
ок: Tromsø: +4

Каждый элемент — объект со status. У успешного есть value, у неудачного — reason. Проверять status обязательно: иначе ошибка уедет дальше как обычное значение.

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

3. forEach не ждёт

Самая коварная из четырёх:

const done = [];
["Oslo", "Tromsø"].forEach(async c => { done.push(await fetchCity(c)); });
console.log("сразу после forEach:", done);
измерено
сразу после forEach: []
через 300 мс:        [ 'Oslo: +4', 'Tromsø: +4' ]

Массив пуст. forEach не умеет ждать async-функции: он запускает их все и идёт дальше, не дожидаясь. Данные появятся — но позже, когда код, который их ждал, уже отработал.

Правильно — обычный цикл:

for (const c of ["Oslo", "Tromsø"]) ok.push(await fetchCity(c));
измерено
после цикла: [ 'Oslo: +4', 'Tromsø: +4' ]

Или Promise.all с map, если запросы независимы и хочется параллельно.

4. Забытый await

измерено
без await: Promise { <pending> }
с await:   Oslo: +4

Вместо значения — объект промиса. В Python это <coroutine object>, здесь Promise { <pending> } — тот же симптом, другое слово.

Как сказать ИИ

Запрос
Эти три запроса независимы. Нужен частичный результат:
если один сервер не ответил, остальные должны дойти.
Используй Promise.allSettled и разбери status.
Запрос
В этом коде forEach с async внутри. Перепиши так,
чтобы результаты действительно дожидались: for...of,
если запросы зависят друг от друга, или Promise.all с map,
если нет.
Запрос
Добавь таймаут 5 секунд на каждый запрос через AbortSignal.timeout.
При таймауте — предупреждение в консоль и пропуск, а не падение.

На что смотреть

Promise.all там, где нужен частичный результат. Одна ошибка теряет всё.

forEach(async ...). Не ждёт. Всегда ошибка, если результат нужен.

Нет .catch и нет try. Отклонённый промис, который никто не поймал, даёт UnhandledPromiseRejection — в Node это падение процесса.

Последовательные await в цикле там, где можно параллельно. Не ошибка, но втрое медленнее на трёх запросах (статья 2.10).

Нет таймаута. Запрос может висеть вечно (статья 6.4). В браузере и Node для этого есть AbortSignal.timeout(ms).

.then() вперемешку с await. Работает, читается плохо. В новом коде — await.

Шпаргалка

  • Промис — обещание результата; resolve успех, reject ошибка.

  • Promise.all роняет всё при первой ошибке, allSettled сохраняет успехи.

  • У allSettled проверяй status: fulfilled → value, rejected → reason.

  • forEach(async ...) не ждёт. Нужен for...of или Promise.all(map).

  • Promise { <pending> } в выводе — забыт await.

  • Непойманный отклонённый промис роняет процесс в Node.