Реализуйте собственный Promise.all. Как он поведёт себя при ошибке одного из промисов?
Короткий ответ
- Принимает массив, возвращает промис с массивом результатов
- Порядок результатов соответствует порядку входных промисов
- Заполнять results по индексу, не через push
- Считать завершённые счётчиком, resolve при полном наборе
- Первый reject немедленно отклоняет общий промис
- Обычные значения оборачивать через Promise.resolve
- Пустой массив — сразу resolve([])
Promise.all собирает результаты в исходном порядке через запись по индексу и счётчик завершённых, падая первым же reject; ключевые краевые случаи — пустой массив и не-промисы во входе.
Как сказать вслух
пример ответаМоя реализация возвращает новый промис и подписывается на каждый элемент массива. Результаты я записываю строго по индексу, потому что промисы завершаются в произвольном порядке, а отдать их нужно в исходном. Отдельным счётчиком считаю, сколько уже выполнилось, и когда счётчик сравнялся с длиной — делаю resolve. Если любой промис упал, сразу отклоняю общий промис этой ошибкой, остальные результаты игнорируются. Ещё учитываю, что в массиве могут быть обычные значения — оборачиваю их в Promise.resolve.
Подробный ответ
Основной ответ
Каркас: функция возвращает new Promise((resolve, reject) => ...), внутри — массив results фиксированной длины и счётчик done. По каждому элементу вызываем Promise.resolve(item).then(...), что корректно обрабатывает и не-промисы, и thenable. В обработчике успеха пишем results[index] = value и инкрементируем done; когда done === items.length, вызываем resolve(results). Ошибку пробрасываем в reject напрямую — семантика fail-fast: первый отказ решает судьбу всего, хотя остальные промисы продолжают выполняться, их результаты просто отбрасываются (отмены в промисах нет). Краевые случаи: пустой массив должен немедленно дать resolve([]); нельзя использовать results.push — порядок сломается; нельзя сравнивать results.length — у разреженного массива она врёт. Следующий вопрос интервьюера обычно про Promise.allSettled (ждёт всех, отдаёт статусы) — отличный повод показать, что вы знаете, когда частичные отказы допустимы.
Ключевые моменты
- Порядок по индексу. Запись results[index] гарантирует исходный порядок независимо от скорости завершения промисов.
- Счётчик, а не length. Готовность определяется счётчиком завершённых; length разреженного массива даёт ложный сигнал.
- Fail-fast. Первый reject отклоняет общий промис; остальные операции не отменяются, но игнорируются.
- Краевые случаи. Пустой массив, обычные значения и thenable во входе — то, на чём проверяют глубину реализации.
Практический контекст
Классика лайв-кодинга: проверяет одновременно понимание промисов, замыканий в цикле и аккуратность с краевыми случаями. В реальной работе паттерн «собрать N параллельных запросов» встречается постоянно — загрузка данных нескольких виджетов, батчи изображений. Полезно уметь продолжить разговор: когда вместо all нужен allSettled, как ограничить конкурентность пулом из k одновременных запросов.
Пример кода
function promiseAll(items) {
return new Promise((resolve, reject) => {
const results = new Array(items.length);
let done = 0;
if (items.length === 0) return resolve(results);
items.forEach((item, i) => {
Promise.resolve(item).then(value => {
results[i] = value;
if (++done === items.length) resolve(results);
}, reject);
});
});
}Частые ошибки
- Используют push и получают результаты в порядке завершения, а не в исходном
- Забывают случай пустого массива — промис зависает навсегда
- Не оборачивают элементы в Promise.resolve и падают на обычных значениях