Если вы когда-либо сталкивались с "адом колбэков" в JavaScript, то промисы — ваш спасательный круг. Это не просто синтаксический сахар, а фундаментальная концепция для работы с асинхронными операциями. В этой статье мы разберем промисы на практических примерах — от базовых до продвинутых — и научимся писать чистый, предсказуемый асинхронный код.
Что такое промис и зачем он нужен?
Промис (Promise) — это специальный объект в JavaScript, который представляет собой результат асинхронной операции, завершившейся успешно или с ошибкой. Вместо передачи колбэков в функцию, мы получаем объект, у которого можно вызвать методы .then(), .catch() и .finally().
Промис может находиться в одном из трех состояний: pending (ожидание), fulfilled (выполнено успешно), rejected (выполнено с ошибкой). После перехода в fulfilled или rejected состояние промиса больше не меняется.
Базовые примеры создания промисов
Создание простого промиса
Давайте создадим свой первый промис, который имитирует загрузку данных:
const loadData = new Promise((resolve, reject) => {
setTimeout(() => {
const success = Math.random() > 0.3;
if (success) {
resolve({ data: 'Данные успешно загружены', status: 200 });
} else {
reject(new Error('Ошибка загрузки данных'));
}
}, 1000);
});
loadData
.then(result => console.log('Успех:', result))
.catch(error => console.error('Ошибка:', error.message))
.finally(() => console.log('Операция завершена'));
Промисы с реальными API
Вот практический пример с использованием Fetch API:
function getUserData(userId) {
return fetch(`https://api.example.com/users/${userId}`)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
});
}
// Использование
getUserData(123)
.then(user => console.log('Пользователь:', user))
.catch(error => console.error('Ошибка:', error));
Цепочки промисов и комбинирование
Одна из самых мощных возможностей промисов — возможность создавать цепочки:
function getUserPosts(userId) {
return fetch(`/api/users/${userId}`)
.then(response => response.json())
.then(user => {
console.log('Найден пользователь:', user.name);
return fetch(`/api/users/${userId}/posts`);
})
.then(response => response.json())
.then(posts => {
console.log(`Найдено ${posts.length} постов`);
return posts;
});
}
Каждый .then() возвращает новый промис, что позволяет создавать длинные цепочки обработки данных без "ада колбэков".
Продвинутые методы работы с промисами
Promise.all() — параллельное выполнение
Когда нужно выполнить несколько асинхронных операций параллельно и дождаться всех результатов:
const fetchUser = fetch('/api/user/1');
const fetchPosts = fetch('/api/posts');
const fetchComments = fetch('/api/comments');
Promise.all([fetchUser, fetchPosts, fetchComments])
.then(responses => Promise.all(responses.map(r => r.json())))
.then(([user, posts, comments]) => {
console.log('Все данные загружены:', { user, posts, comments });
})
.catch(error => console.error('Одна из загрузок завершилась ошибкой:', error));
Promise.race() — гонка промисов
Полезно, когда нужно получить результат первого выполнившегося промиса:
const timeout = new Promise((_, reject) => {
setTimeout(() => reject(new Error('Таймаут!')), 5000);
});
const apiRequest = fetch('/api/data');
Promise.race([apiRequest, timeout])
.then(response => console.log('Данные получены вовремя'))
.catch(error => console.error('Ошибка:', error.message));
Promise.allSettled() — все результаты
Получить результаты всех промисов, независимо от их успешности:
Promise.allSettled([
Promise.resolve('Успех 1'),
Promise.reject(new Error('Ошибка')),
Promise.resolve('Успех 2')
])
.then(results => {
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`Промис ${index}:`, result.value);
} else {
console.log(`Промис ${index} отклонен:`, result.reason.message);
}
});
});
Async/await — синтаксический сахар над промисами
Современный способ работы с промисами, который делает код более читаемым:
async function loadUserData(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error(`Ошибка HTTP: ${response.status}`);
}
const user = await response.json();
const postsResponse = await fetch(`/api/users/${userId}/posts`);
const posts = await postsResponse.json();
return { user, posts };
} catch (error) {
console.error('Ошибка загрузки:', error);
throw error;
} finally {
console.log('Загрузка данных пользователя завершена');
}
}
// Использование
loadUserData(123)
.then(data => console.log('Данные:', data))
.catch(error => console.error('Финальная ошибка:', error));
Функция с async всегда возвращает промис. Ключевое слово await можно использовать только внутри async-функций.
Распространенные ошибки и лучшие практики
- Не забывайте обрабатывать ошибки: Всегда добавляйте
.catch()или используйте try/catch с async/await - Избегайте вложенных промисов: Используйте цепочки или async/await для плоской структуры
- Не создавайте промисы без необходимости: Если операция синхронная, не оборачивайте ее в промис
- Используйте Promise.all для параллельных операций: Не делайте последовательные await для независимых операций
FAQ — Часто задаваемые вопросы
В чем разница между Promise.all() и Promise.allSettled()?
Promise.all() завершается с ошибкой, если хотя бы один промис отклонен. Promise.allSettled() всегда ждет завершения всех промисов и возвращает массив с результатами каждого.
Можно ли отменить промис?
Стандартные промисы JavaScript не поддерживают отмену. Для этого можно использовать AbortController с Fetch API или сторонние реализации отменяемых промисов.
Чем async/await лучше обычных промисов?
Async/await делает асинхронный код более читаемым и похожим на синхронный. Однако это всего лишь синтаксический сахар — под капотом все равно работают промисы.
Что такое "обещание" в контексте промисов?
Промис (Promise) переводится как "обещание". Это действительно обещание JavaScript предоставить результат операции в будущем — успешный или с ошибкой.
Как обрабатывать несколько ошибок в цепочке промисов?
Можно использовать несколько .catch() в цепочке или один общий в конце. Каждый .catch() может обработать ошибку и либо вернуть значение, либо пробросить ошибку дальше.