Promise и async/await
Что такое Promise
Promise (промис) - это объект, который представляет результат асинхронной операции, ещё не известный в момент вызова. Это «обещание» вернуть значение в будущем: либо успешный результат, либо ошибку.
Аналогия: вы заказываете кофе и получаете чек с номером. Сам кофе ещё не готов, но у вас на руках объект (чек), по которому вы заберёте результат, когда он будет готов. Промис - такой же «чек» на будущее значение.
Зачем он нужен: раньше асинхронность писали через колбэки, и вложенные вызовы превращались в «callback hell» - лесенку из функций. Промис делает асинхронный код линейным: операции выстраиваются в цепочку .then(), а ошибки собираются в одном .catch().
// было: колбэки внутри колбэков
getUser(id, (user) => {
getOrders(user, (orders) => {
getDetails(orders, (details) => { /* ... */ })
})
})
// стало: плоская цепочка промисов
getUser(id)
.then(user => getOrders(user))
.then(orders => getDetails(orders))
.catch(err => console.error(err))
Ключевое: промис не делает код синхронным и не блокирует поток. Он лишь даёт удобный способ подписаться на результат, который придёт позже.
Promise - три состояния
const p = new Promise((resolve, reject) => {
// pending → fulfilled
resolve("данные")
// или pending → rejected
reject(new Error("что-то пошло не так"))
})
p
.then(data => console.log(data))
.catch(err => console.error(err))
.finally(() => console.log("всегда выполнится"))
Цепочки
fetch("/api/user")
.then(res => res.json()) // возвращает новый Promise
.then(user => user.name)
.then(name => console.log(name))
.catch(err => console.error(err)) // ловит любую ошибку в цепочке
async/await - синтаксический сахар
async/await - это надстройка над промисами, которая позволяет писать асинхронный код так, будто он синхронный. Под капотом это всё те же промисы, просто без .then().
async function loadUser() {
try {
const res = await fetch("/api/user") // ждёт Promise
const user = await res.json()
return user.name
} catch (err) {
console.error(err)
}
}
Два правила, на которых всё держится:
- Функция с
asyncвсегда возвращает Promise. Любоеreturn valueавтоматически оборачивается вPromise.resolve(value), аthrow- в отклонённый промис. Поэтому вызывающий код всё равно работает с промисом:loadUser().then(...). awaitставится перед промисом и приостанавливает выполнение только этой async-функции до тех пор, пока промис не завершится. Результатawait- это распакованное значение (то, чем промис зарезолвился).
Как это работает внутри
await не блокирует поток. Когда движок доходит до await, он «ставит на паузу» текущую функцию, возвращает управление наружу (остальной синхронный код продолжает выполняться), а продолжение функции ставит в очередь микрозадач - то же место, куда попадают колбэки .then(). Когда промис завершается, функция возобновляется с того же места.
async function demo() {
console.log("1")
await Promise.resolve()
console.log("3") // продолжение - микрозадача, выполнится после синхронного кода
}
console.log("0")
demo()
console.log("2")
// порядок: 0, 1, 2, 3
То есть await x примерно эквивалентно x.then(результат => /* всё, что идёт после await */) - просто записано линейно.
Чем отличается от обычных промисов
Функционально - ничем, это тот же механизм. Разница в читаемости и в работе с ошибками:
Промисы (.then) | async/await | |
|---|---|---|
| Стиль | цепочка колбэков | линейный, «синхронный» вид |
| Ошибки | .catch() | обычный try/catch |
| Промежуточные значения | живут внутри замыканий .then | обычные переменные в области функции |
| Отладка | стек обрывается на колбэках | привычный стек, точки останова работают как в синхронном коде |
// Промисы: значения приходится «протаскивать» через цепочку
function load() {
return fetch("/api/user")
.then(res => res.json())
.then(user => fetch(`/api/orders/${user.id}`))
.then(res => res.json())
}
// async/await: те же шаги, но переменные доступны напрямую
async function load() {
const user = await (await fetch("/api/user")).json()
const orders = await (await fetch(`/api/orders/${user.id}`)).json()
return orders
}
Важно: удобный линейный вид легко превращается в случайную последовательность там, где можно было распараллелить. Если задачи независимы, не пишите await подряд - используйте Promise.all (см. ниже).
Параллельное выполнение
// ❌ Последовательно (медленно)
const a = await fetchA()
const b = await fetchB()
// ✅ Параллельно
const [a, b] = await Promise.all([fetchA(), fetchB()])
Promise.allSettled, race, any
// allSettled - ждёт всех, не падает при ошибке
const results = await Promise.allSettled([p1, p2, p3])
// race - возвращает первый завершившийся
const first = await Promise.race([p1, p2])
// any - возвращает первый успешный (ES2021)
const fastest = await Promise.any([p1, p2])