DarkRiDDeR12 мин

Как объяснить Promise.all: сначала модель, затем рецепт и проверка

JavaScriptНаставничество

Симптом знакомый: объяснение начинается с await Promise.all(tasks), код копируют в обработчик, а после первой ошибки считают оставшуюся работу отменённой. Цена ошибки — повторный разбор уже начатых действий, лишняя логика очистки и обучение рецепту, который ломается при первом изменении условий.

Причина не в одной строке JavaScript. Рецепт скрывает модель: Promise.all собирает исходы входных promises в один aggregate promise, но не получает отдельного механизма остановки внешней работы. Проверка — попросить объяснить, что произойдёт с ещё не завершившимся входом после первой ошибки. Действие — строить объяснение в пять коротких шагов.

Исходная задача и узкая граница

Исходная задача synthetic: получить два независимых значения и не продолжать зависимый расчёт, если одно значение не получено. Здесь promise — объект будущего результата; aggregate promise — один promise, который описывает результат набора. Мы не моделируем HTTP, файл или базу: важна только граница наблюдения за двумя значениями в памяти. Такой пример не учит отмене, потому что отмены в нём нет.

Схема объяснения: исходная задача ведёт к модели aggregate promise, затем к контрпримеру с оставшимся входом, короткому упражнению и точной проверке hand-off.
Путь специально отделяет результат aggregate promise от жизненного цикла каждого входа. Это предотвращает подмену модели готовым рецептом.
Пять частей объяснения
ЧастьВопрос читателюАртефактОшибка без части
Задачачто должно дождаться двух значений?два fixed inputsкод кажется универсальным
Моделькто хранит общий исход?aggregate promiseпутают ожидание и остановку
Контрпримерчто остаётся после reject?ручное завершение входаошибка выглядит как отмена
Упражнениекакую границу надо назвать?одно предложениепроверяют термин
Проверкачто передаём дальше?specific gap или hand-offобратная связь расплывчата

Минимальная модель в исполняемом примере

import { runFixedPromiseExercise } from './upgrade-2025-10.mjs';

const result = await runFixedPromiseExercise();
console.log(result.trace);
// ['aggregate-rejected', 'remaining-input-fulfilled']
// Fixed in-memory promises only; no clock, HTTP, files or side effects.

Первый вход уже отклонён с fixed ошибкой. Второй намеренно удерживается функцией completeRemaining. Мы сначала ждём обработанный aggregate reject, потом вручную завершаем второй вход. Порядок trace — проверяемая модель: aggregate уже сообщил об ошибке, но второй input всё ещё может завершиться. Это не измеряет скорость и не описывает реальную интеграцию; это демонстрация того, какую границу обязан назвать автор объяснения.

Контрпример важнее второго рецепта

Неправильная фраза звучит удобно: «Promise.all остановит всё при ошибке». Контрпример не спорит с удобством, он проверяет условие. У aggregate promise есть ранний rejected исход; у созданного ранее input нет в этом вызове отдельной команды cancel. Поэтому после reject нельзя делать вывод, что внешний запрос, таймер или вычисление исчезли. Для их остановки проектируют отдельный контракт и проверяют его отдельно.

Короткое упражнение вместо пересказа

  1. Дайте только задачу: «два значения нужны до зависимого расчёта».
  2. Покажите trace и попросите назвать два разных объекта: aggregate и remaining input.
  3. Попросите закончить фразу: «Для остановки внешней работы нужен отдельный …».
  4. Проверьте не словом «верно», а fixed answer: Promise.all-observes-settlement-not-cancellation.
  5. Передайте дальше только synthetic карточку с model, counterexample и точным пробелом, если ответ неполный.

Проверяемая обратная связь

import { createFixedTeachingAttempt, checkFixedTeachingAttempt } from './upgrade-2025-10.mjs';

const report = checkFixedTeachingAttempt(
  createFixedTeachingAttempt('wrong-cancellation-v1'),
);
console.log({
  accepted: report.accepted,
  reasons: report.reasons,
  handoff: report.handoff,
});
// { accepted: false, reasons: ['wrong-boundary'], handoff: 'return-with-specific-gap' }

Такой feedback не говорит, что попытка «плохая». Он называет ровно одну отсутствующую границу: aggregate не равен отмене. Positive result здесь скромный: корректный fixed hand-off для human review, а не доказательство, что кто-то научился лучше или что команда стала эффективнее.

Проверка объяснения до публикации

Перед публикацией полезно прогнать текст как маленький тест, а не как лекцию. Уберите из него код и оставьте четыре вопроса: какая исходная задача, какой объект агрегирует исходы, какой контрпример запрещает слишком широкий вывод и кто владеет отменой. Если на один вопрос нельзя ответить по тексту, фрагмент кода ещё не объяснение. Он может быть верным для исходного случая, но не передаёт условие безопасного переноса.

Затем верните код и проверьте соответствие строк модели. Массив в Promise.all — набор inputs; переменная результата — aggregate; ветка catch — реакция зависимой логики на его rejected outcome. В этой минимальной форме нет claim о поведении сети. Такое ограничение кажется менее эффектным, чем «запускаем параллельно и всё отменится», но оно дешевле в сопровождении: читатель не получает несуществующую гарантию и знает, какой контракт искать дальше.

Чек-лист переноса рецепта
ВопросОтвет в моделиЧто сделать при отсутствии
Какие значения нужны вместе?fixed left и right inputsсузить задачу
Что меняет Promise.all?aggregate outcomeне писать про ресурс
Что доказывает reject?не начинать зависимый расчётдобавить контрпример
Кто отменяет работу?не определено этим вызовомспроектировать отдельный contract

Ограничение и следующий шаг

MDN в закреплённой версии описывает успешный исход всех inputs, первый reject и порядок результатов; ECMA-262 задаёт алгоритм combinator. Ни один источник не даёт готового протокола отмены. Поэтому следующий шаг — взять один собственный API и отдельно выписать: кто создаёт работу, кто имеет право остановить её, как выглядит подтверждение остановки. Не переносите synthetic trace как production design.

Проверяемые источники

  • ECMA-262, 16th edition (June 2025), §27.2.4.1 Promise.all — версия: 16th edition, June 2025, immutable published PDF. Алгоритм Promise.all создаёт одну capability, обходит iterable и передаёт его элементы в PerformPromiseAll; результат завершается ошибкой через reject capability. Граница: Спецификация описывает семантику языка. Она не проектирует отмену работы, timeout, retry или API конкретного приложения.
  • MDN Promise.all(), immutable content commit 3fad0447 (19 August 2025) — версия: mdn/content commit 3fad0447b4901e28fe88769976787d8d8b87d66d, 2025-08-19. Promise.all выполняется успешно после выполнения всех входов либо отклоняется на первой ошибке; результат успеха сохраняет порядок входного iterable, а allSettled ждёт все исходы. Граница: Документация не утверждает, что Promise.all отменяет уже начатую внешнюю работу или заменяет протокол остановки.