Промисы в JavaScript давно перестали быть новинкой. Но многие разработчики используют лишь базовые возможности: then, catch, finally. А ведь есть ещё куча продвинутых методов, которые делают код чище, надёжнее и эффективнее. В этой статье разберём, как выжать максимум из промисов и не наступить на грабли.
Promise.allSettled: когда нужно всё и сразу
Promise.all хорош, но он падает при первой же ошибке. А если вам нужно дождаться выполнения всех промисов, даже если некоторые упали? Тут на помощь приходит Promise.allSettled. Он возвращает массив результатов, где каждый элемент имеет статус fulfilled или rejected.
const promises = [
fetch('/api/user'),
fetch('/api/posts'),
Promise.reject(new Error('Что-то пошло не так'))
];
Promise.allSettled(promises)
.then(results => {
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`Промис ${index} выполнен:`, result.value);
} else {
console.error(`Промис ${index} отклонён:`, result.reason);
}
});
});
Этот метод идеален, когда вы делаете несколько независимых запросов и хотите обработать каждый, несмотря на ошибки.
Promise.any: первый успешный
Promise.any ждёт первый успешно выполненный промис и игнорирует отклонённые. Если все промисы отклонены, возвращается AggregateError. Это удобно, когда у вас несколько источников данных и достаточно получить ответ от самого быстрого.
const mirrors = [
fetch('https://mirror1.example.com/data'),
fetch('https://mirror2.example.com/data'),
fetch('https://mirror3.example.com/data')
];
Promise.any(mirrors)
.then(response => response.json())
.then(data => console.log('Данные получены:', data))
.catch(error => console.error('Все зеркала недоступны:', error));
Обратите внимание: Promise.any не ждёт остальные промисы после первого успеха, но они продолжают выполняться в фоне. Это может быть полезно или вредно в зависимости от задачи.
Обработка ошибок: тонкости и лайфхаки
Ошибки в промисах — больная тема. Многие забывают добавлять catch, что приводит к необработанным отказам. Но есть и более тонкие моменты.
Во-первых, ошибка, брошенная внутри then, перехватывается следующим catch. Но если вы используете несколько then, ошибка пропустит все успешные обработчики и попадёт в ближайший catch.
Promise.resolve('ok')
.then(result => {
console.log(result); // 'ok'
throw new Error('Упс');
})
.then(() => console.log('Этот then пропущен'))
.catch(error => console.error('Поймали:', error.message)); // 'Упс'
Во-вторых, если вы хотите обработать ошибку и продолжить цепочку, возвращайте из catch значение или новый промис.
fetch('/api/data')
.catch(error => {
console.warn('Не удалось загрузить, используем кэш');
return getFromCache();
})
.then(data => console.log(data));
Отмена промисов и таймауты
Стандартные промисы не поддерживают отмену. Но это не значит, что нельзя реализовать таймаут или отмену запроса. Для этого используют AbortController или гонку с setTimeout.
Пример с таймаутом:
function withTimeout(promise, ms) {
return Promise.race([
promise,
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Таймаут')), ms)
)
]);
}
withTimeout(fetch('/api/slow'), 5000)
.then(response => console.log('Успели'))
.catch(error => console.error(error.message)); // 'Таймаут'
Для отмены fetch используйте AbortController:
const controller = new AbortController();
const signal = controller.signal;
fetch('/api/data', { signal })
.then(response => response.json())
.catch(error => {
if (error.name === 'AbortError') {
console.log('Запрос отменён');
}
});
// Отмена через 2 секунды
setTimeout(() => controller.abort(), 2000);
Продвинутые шаблоны: последовательные и параллельные очереди
Иногда нужно выполнять промисы последовательно, чтобы не перегружать сервер. Или наоборот, ограничить количество одновременных запросов. Вот пример функции, которая выполняет массив промисов с ограничением параллелизма:
async function limitedParallel(promises, limit) {
const results = [];
const executing = [];
for (const promise of promises) {
const p = Promise.resolve().then(() => promise());
results.push(p);
if (limit <= promises.length) {
const e = p.then(() => executing.splice(executing.indexOf(e), 1));
executing.push(e);
if (executing.length >= limit) {
await Promise.race(executing);
}
}
}
return Promise.all(results);
}
Такой подход часто используется в скраперах или при работе с API, где есть лимиты.
| Метод | Когда использовать |
|---|---|
| Promise.all | Когда все промисы должны выполниться успешно, иначе результат не нужен |
| Promise.allSettled | Когда нужно обработать каждый промис независимо от ошибок |
| Promise.race | Когда важен первый завершившийся промис (успех или ошибка) |
| Promise.any | Когда нужен первый успешный промис, а ошибки можно игнорировать |
Итог
Промисы — мощный инструмент, но их истинная сила раскрывается в продвинутых методах. Используйте Promise.allSettled для сбора всех результатов, Promise.any для быстрого успеха, не забывайте про таймауты и отмену. И всегда обрабатывайте ошибки. Тогда ваш асинхронный код станет надёжным и предсказуемым.
Студия WNDER



