Симптом механического объяснения — читатель знает, что написать, но не может предсказать состояние набора после reject. Цена ошибки — код, который очищает несуществующее состояние или считает внешний вызов остановленным без подтверждения, а затем требует дорогостоящего расследования границ владения.
Причина — смешение трёх уровней: входной promise, aggregate promise и внешняя операция, которая могла создать вход. Проверка — нарисовать для каждого уровня собственный исход и спросить, какое действие соединяет их. Действие — объяснять Promise.all как наблюдатель набора результатов, а отмену оставлять отдельным протоколом с владельцем.
Три объекта, которые нельзя назвать одним словом
Входной promise представляет один будущий исход. Aggregate promise — новый объект, который создаёт Promise.all(iterable). Внешняя операция — условная работа за пределами этой учебной модели; она может вообще отсутствовать, как в нашем ручном fixed input. Разделение полезно не терминологией, а вопросом владения: чей исход меняет combinator и у кого есть право потребовать остановку работы.
| Наблюдение | Допустимый вывод | Нельзя утверждать | Следующая проверка |
|---|---|---|---|
| вход A rejected | aggregate может перейти в rejected | вход B отменён | наблюдать B отдельно |
| вход B fulfilled позже | B имеет собственный исход | aggregate станет fulfilled | прочитать aggregate outcome |
| aggregate rejected | зависимый расчёт можно не начинать | внешняя работа остановлена | найти cancel contract |
| allSettled fulfilled | доступны все исходы | ошибки исчезли | разобрать каждый status |
Что фиксирует спецификация
В ECMA-262 16th edition Promise.all получает iterable, создаёт promise capability и передаёт iterator в PerformPromiseAll. Это важная, но узкая опора: combinator работает с promises и capability результата. Она не говорит «закрой сокет», «прерви вычисление» или «освободи файл». Поэтому из языковой семантики нельзя тихо вывести контракт ресурса приложения.
MDN закреплённым commit формулирует тот же видимый результат: aggregate fulfils после всех входов и rejects при первой ошибке; порядок значений следует порядку iterable, а не скорости завершения. Слова «первая ошибка» относятся к observable outcome aggregate. Они не превращают уже созданные входы в отменённые. Это различие и есть минимальная модель, которую надо передать до примера с await.
Контрпример с ручным управлением
import { runFixedPromiseExercise } from './upgrade-2025-10.mjs';
const { trace, interpretation } = await runFixedPromiseExercise();
console.log({ trace, interpretation });
// aggregate-rejected появляется раньше remaining-input-fulfilled
В этом примере нет сети и нет таймера, поэтому нельзя спрятать объяснение за планировщиком. Второй input завершается только после вызова сохранённой функции. Если aggregate reject мог бы отменить input сам, второй элемент trace не появился бы после первого. Но он появляется, потому что ручной владелец всё ещё способен завершить свой promise. Это ровно контрпример к ложному переносу «short-circuit результата» на «остановку всех работ».
Почему allSettled не является отменой
Иногда ложную модель меняют на другой рецепт: «тогда всегда используем allSettled». Он решает другой вопрос: дождаться всех исходов, чтобы получить statuses. Он не добавляет прав остановки и не делает зависимый расчёт безопасным автоматически. Выбор между all и allSettled начинается с нужного результата aggregate: ранний stop дальнейшей логики или полный список исходов. Отмена живёт в третьем, отдельном вопросе.
Упражнение на причинность
- Прочитайте trace без кода и отметьте, чей исход зафиксирован на каждой строке.
- Сформулируйте правило: aggregate reject запрещает какой именно зависимый расчёт?
- Назовите факт, которого в trace нет: «внешняя работа отменена».
- Добавьте в synthetic карточку отдельное поле
cancellationOwner; в текущей модели оно намеренно отсутствует. - Сверьте вывод с negative attempt: ответ «Promise.all-cancels-everything» обязан вернуть
wrong-boundary.
Граница, которую стоит оставить в коде
Узкая инженерная польза модели — не в том, чтобы запрещать Promise.all. Она помогает отделить ветвление зависимой логики от управления ресурсом. Когда оба вопроса названы, API можно обсуждать предметно: нужен ли отдельный signal, кто его создаёт, что значит acknowledgement. В этом draft-пакете такого API нет; positive result — только synthetic hand-off с указанной неизвестной границей.
Как не превратить модель в новую догму
Модель не требует ждать все результаты всегда. Если зависимый расчёт бессмыслен без любого одного значения, ранний rejected aggregate даёт ясную точку остановки именно для этого расчёта. Если для отчёта нужны все statuses, подходит другой aggregate outcome. Если нужно прекратить внешнее действие, появляется четвёртый объект — cancellation contract. Эти три решения могут находиться в одном модуле, но отвечают на разные вопросы и должны иметь разные проверки.
Полезный self-check: замените слово «параллельно» на конкретный факт. Для Promise.all это не обещание времени выполнения, а способ собрать несколько promises. Замените слово «отменится» на имя команды и acknowledgement, если они существуют. Если имени нет, честный текст говорит «не определено». Такая формулировка даёт читателю точку для проектирования, а не создаёт ложное чувство завершённости после одной конструкции языка.
| Нужный эффект | Инструмент модели | Критерий проверки |
|---|---|---|
| не начинать зависимый расчёт | aggregate reject | ветка catch не вычисляет следующий шаг |
| получить каждый исход | allSettled | в списке есть status каждого input |
| остановить внешнюю работу | отдельный contract | владелец и acknowledgement названы |
| собрать значения по порядку | Promise.all fulfill | массив следует iterable, не скорости |
В этом разборе используются только заранее заданные объекты JavaScript в памяти. Ни один шаг не обращается к HTTP, диску, часам, телеметрии, CI, Git, production или данным людей; результат остаётся учебным synthetic hand-off.
Проверяемые источники
- 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 отменяет уже начатую внешнюю работу или заменяет протокол остановки.