Промисы — это как обещания в жизни: либо выполняются, либо нет. В JavaScript они помогают управлять асинхронными операциями, не превращая код в «ад колбэков». Но просто знать базовые .then() и .catch() — мало. Сегодня разберем продвинутые методы, которые сделают ваш код чище, быстрее и надежнее. Если вы уже умеете создавать промисы и ловить ошибки, эта статья — следующий шаг.
Цепочки промисов: как не заблудиться в then
Цепочки промисов — это как конвейер на заводе: каждая операция ждет завершения предыдущей. Но часто разработчики делают ошибки, которые ломают всю цепочку. Давайте посмотрим на правильный подход.
// Плохо: вложенные промисы
fetch('/user')
.then(response => response.json())
.then(user => {
fetch('/posts?userId=' + user.id)
.then(response => response.json())
.then(posts => {
console.log(posts);
});
});
// Хорошо: плоская цепочка
fetch('/user')
.then(response => response.json())
.then(user => fetch('/posts?userId=' + user.id))
.then(response => response.json())
.then(posts => console.log(posts));
Во втором случае каждый .then() возвращает новый промис, и цепочка остается плоской. Это проще читать и отлаживать. Запомните: если .then() возвращает промис, следующий .then() дождется его выполнения.
Комбинирование промисов: Promise.all, race, allSettled
Иногда нужно запустить несколько асинхронных операций одновременно. Например, загрузить данные о пользователе и его постах. Для этого есть специальные методы.
| Метод | Что делает | Когда использовать |
|---|---|---|
| Promise.all | Ждет все промисы, если один упал — падает всё | Когда нужны все данные, и без одного всё ломается |
| Promise.race | Возвращает первый завершенный промис (успех или ошибку) | Когда нужен только самый быстрый результат (например, таймаут) |
| Promise.allSettled | Ждет все, но не падает, даже если некоторые упали | Когда нужно получить результаты всех, даже с ошибками |
Вот пример с Promise.all:
const userPromise = fetch('/user').then(r => r.json());
const postsPromise = fetch('/posts').then(r => r.json());
Promise.all([userPromise, postsPromise])
.then(([user, posts]) => {
console.log(user, posts);
})
.catch(error => {
console.error('Что-то пошло не так:', error);
});
А если вы не хотите, чтобы одна ошибка рушила всё, используйте allSettled:
Promise.allSettled([userPromise, postsPromise])
.then(results => {
results.forEach(result => {
if (result.status === 'fulfilled') {
console.log('Успех:', result.value);
} else {
console.log('Ошибка:', result.reason);
}
});
});
Отмена промисов: как убить зависший запрос
Стандартные промисы не умеют отменяться. Но это не значит, что нельзя сделать отмену вручную. Часто это нужно, когда пользователь ушел со страницы или передумал. Используем паттерн с флагом или AbortController.
// Используем AbortController для fetch
const controller = new AbortController();
const signal = controller.signal;
fetch('/data', { signal })
.then(response => response.json())
.then(data => console.log(data))
.catch(error => {
if (error.name === 'AbortError') {
console.log('Запрос отменен');
} else {
console.error(error);
}
});
// Отменяем через 2 секунды
setTimeout(() => controller.abort(), 2000);
Для своих промисов можно сделать обертку с отменой:
function cancellablePromise(executor) {
let isCancelled = false;
const promise = new Promise((resolve, reject) => {
executor(
value => isCancelled ? reject({ cancelled: true }) : resolve(value),
reason => isCancelled ? reject({ cancelled: true }) : reject(reason)
);
});
return {
promise,
cancel() {
isCancelled = true;
}
};
}
// Использование
const { promise, cancel } = cancellablePromise((resolve) => {
setTimeout(() => resolve('Готово'), 3000);
});
promise.then(console.log).catch(err => console.log('Отменено?', err.cancelled));
cancel(); // отменяем сразу
Обработка ошибок: как не уронить приложение
Ошибки в промисах — это как гром среди ясного неба. Если их не поймать, приложение может молча упасть. Вот несколько продвинутых приемов.
Во-первых, всегда добавляйте .catch() в конец цепочки. Но лучше использовать finally для очистки:
fetch('/data')
.then(response => response.json())
.then(data => process(data))
.catch(error => console.error('Ошибка:', error))
.finally(() => {
// Скрыть спиннер, очистить ресурсы
console.log('Операция завершена');
});
Во-вторых, не забывайте о том, что .catch() сам может вернуть значение, которое пойдет дальше по цепочке. Это удобно для fallback:
fetch('/settings')
.then(response => {
if (!response.ok) throw new Error('Ошибка сети');
return response.json();
})
.catch(() => ({ theme: 'light' })) // дефолтные настройки
.then(settings => applySettings(settings));
Промисификация: превращаем колбэки в промисы
Старые API часто используют колбэки. Чтобы работать с ними в современном стиле, нужно обернуть их в промисы. Это называется «промисификация».
const fs = require('fs');
function readFilePromise(path, encoding = 'utf8') {
return new Promise((resolve, reject) => {
fs.readFile(path, encoding, (err, data) => {
if (err) reject(err);
else resolve(data);
});
});
}
// Использование
readFilePromise('file.txt')
.then(content => console.log(content))
.catch(err => console.error(err));
В Node.js уже есть promisify из модуля util:
const { promisify } = require('util');
const readFile = promisify(fs.readFile);
readFile('file.txt', 'utf8')
.then(content => console.log(content));
Этот прием особенно полезен, когда вы работаете с библиотеками, которые не обновились до промисов.
Продвинутые паттерны: серийное выполнение и ограничение параллелизма
Иногда нужно выполнить промисы строго по очереди, а не параллельно. Например, чтобы не перегрузить сервер. Для этого можно использовать цикл с await (в асинхронной функции) или reduce с цепочкой.
// Последовательное выполнение
async function runInSequence(tasks) {
const results = [];
for (const task of tasks) {
results.push(await task());
}
return results;
}
// Пример
const tasks = [
() => fetch('/api/1'),
() => fetch('/api/2'),
() => fetch('/api/3')
];
runInSequence(tasks).then(results => console.log(results));
А если нужно ограничить параллелизм (например, максимум 2 запроса одновременно), вот такой трюк:
async function runWithLimit(tasks, limit) {
const results = [];
const executing = [];
for (const task of tasks) {
const promise = task().then(result => {
results.push(result);
});
executing.push(promise);
if (executing.length >= limit) {
await Promise.race(executing); // ждем хотя бы один завершится
// Убираем завершившиеся из списка
for (let i = executing.length - 1; i >= 0; i--) {
if (executing[i] === promise) continue;
// Простая проверка: если промис завершен, он уже не в pending
// Но для простоты можно просто сдвигать по индексу
}
}
}
await Promise.all(executing);
return results;
}
Этот код сложноват, но идея ясна: используем Promise.race, чтобы дождаться освобождения слота.
Итоги: что мы узнали
Промисы — это мощный инструмент, но с подводными камнями. Мы разобрали:
- Как строить плоские цепочки и не тонуть в вложенности.
- Как комбинировать промисы с помощью all, race, allSettled.
- Как отменять промисы, используя AbortController или свои обертки.
- Как правильно обрабатывать ошибки и использовать finally.
- Как превращать колбэки в промисы.
- Как выполнять промисы последовательно или с ограничением параллелизма.
Теперь вы готовы писать более надежный и читаемый асинхронный код. Экспериментируйте, пробуйте разные подходы — и ваши промисы никогда не будут «висеть» без дела.



