async-функция возвращает промис, а await позволяет получить его результат. При этом код после await продолжится позже, даже если результат уже готов. Чтобы разобраться в порядке выполнения, проследим пять строк вывода, а затем поймаем ошибку из асинхронной функции.
Примеры работают без сервера и таймеров. Сохрани любой блок JavaScript в отдельный файл example.js и запусти node example.js. Если пока трудно читать объявления функций, начни с уроков JavaScript.
Что такое Promise: состояние и результат
Promise, или промис, хранит состояние операции и позволяет получить её результат, когда он будет готов. Например, функция загрузки возвращает промис сразу, а данные появляются позже. Промис может быть и уже завершённым.
| Состояние | Что означает | Что делает await |
|---|---|---|
pending | Результата ещё нет | Приостанавливает эту async-функцию до завершения промиса |
fulfilled | Операция завершилась успешно | При возобновлении функции даёт значение результата |
rejected | Операция завершилась отказом | При возобновлении выбрасывает причину отказа |
После перехода в fulfilled или rejected состояние уже не меняется. Причиной отказа может быть любое значение, но объект Error удобнее: у него есть сообщение и стек вызовов. Эти состояния описаны в справке MDN по Promise.
Для начала создадим готовый результат через Promise.resolve(). Метод .then() принимает функцию, которая получит это значение:
const answer = Promise.resolve(42)
answer
.then((value) => {
console.log(value)
})
.catch(console.error)
Вывод:
42
В переменной answer находится промис. Число 42 получает обработчик .then(). Он вызывается после завершения текущего синхронного кода. .catch(console.error) выводит ошибку, если цепочка завершится отказом.
Что возвращает async-функция
Объявление async меняет результат вызова функции: вызывающий код всегда получает промис. Если тело выполняет return 42, значением этого промиса будет 42.
async function getAnswer() {
return 42
}
const result = getAnswer()
console.log(result instanceof Promise)
result.then((value) => console.log(value)).catch(console.error)
Вывод:
true
42
Без return промис успешно завершится со значением undefined. Неперехваченный throw внутри async-функции отклонит её промис. Если вернуть другой промис, результат async-функции будет зависеть от его исхода. Это поведение определено для async function.
Само слово async не переносит вычисления в другой поток. Код до первого await выполняется сразу при вызове функции; длинный цикл на этом участке всё ещё занимает поток JavaScript.
В каком порядке выполняется код с await
await приостанавливает содержащую его async-функцию. Вызывающий код продолжает работу, а оставшаяся часть функции возобновится через микрозадачу: отдельный шаг, который выполняется после освобождения стека текущих вызовов.
Проверим это на уже готовом промисе:
async function showAnswer() {
console.log('B')
const answer = await Promise.resolve(42)
console.log('D', answer)
}
console.log('A')
const result = showAnswer()
console.log('C')
result.then(() => console.log('E')).catch(console.error)
Вывод:
A
B
C
D 42
E
- Скрипт печатает
A, затем вызываетshowAnswer(). Её начало выполняется сразу и печатаетB. - На
awaitфункция приостанавливается. Скрипт получает её промис, печатаетCи подключает обработчик.then(). - После синхронного кода возобновляется
showAnswer(): получает42, печатаетD 42и завершается. Затем обработчик её промиса печатаетE.
Готовое значение не отменяет паузу на await
C появляется раньше D 42, хотя промис уже выполнен. E ждёт завершения всей showAnswer().Пауза будет и при await 42: число тоже допустимо, но продолжение всё равно откладывается. Обычно так писать незачем; этот случай помогает проверить, что порядок задаёт сам await, а не скорость загрузки. Подробности — в описании await на MDN.
Здесь нет соревнования таймеров и запросов. Порядок A → B → C → D 42 → E следует из зависимостей в коде, поэтому для его объяснения не нужны предположения о миллисекундах.
Как поймать ошибку через try/catch
Когда ожидаемый промис отклоняется, await выбрасывает причину отказа. Поэтому try/catch вокруг await ловит ошибку и позволяет решить, что делать дальше.
В этом примере источник данных намеренно сообщает об отсутствующем имени:
async function readName() {
throw new Error('Имя не указано')
}
async function showName() {
try {
const name = await readName()
console.log(name)
} catch (error) {
console.log('Ошибка:', error.message)
}
console.log('Форма открыта')
}
showName().catch(console.error)
Вывод:
Ошибка: Имя не указано
Форма открыта
Строка console.log(name) пропускается: присваивание не завершилось. Управление переходит в catch. После него функция продолжает работу, потому что обработчик не делает return и не выбрасывает новую ошибку.
Отказ промиса становится исключением на await
catch. Строка «Форма открыта» подтверждает, что после обработки выполнение продолжилось.В этом сценарии ошибку обработали на уровне формы. Если вызывающий код должен узнать об отказе, передай его дальше через throw error. Сам по себе вывод сообщения в catch не сохраняет ошибочный исход: без нового throw или отказа функция завершится успешно.
Почему try/catch без await пропускает отказ
try/catch ловит исключение, выброшенное во время выполнения его блока. Простой вызов async-функции возвращает промис, даже если внутри неё сразу выполнился throw.
Здесь внутренний обработчик не сработает:
async function readName() {
throw new Error('Имя не указано')
}
async function forwardName() {
try {
return readName()
} catch (error) {
console.log('Внутренний catch:', error.message)
throw error
}
}
forwardName().catch((error) => {
console.log('Внешний catch:', error.message)
})
Вывод:
Внешний catch: Имя не указано
forwardName() передаёт исход промиса вызывающему коду. Чтобы перехватить отказ именно внутри forwardName, замени строку на return await readName(). Тогда отказ превратится в исключение внутри try. Если локальная обработка не нужна, возвращать промис напрямую нормально; ошибку принимает вызывающая сторона.
Метод .catch() тоже возвращает новый промис. Если его обработчик вернёт запасное значение, цепочка продолжится успешно с этим значением; если сделает throw, отказ пойдёт дальше. Так работает Promise.prototype.catch().
Для сетевого кода есть дополнительное условие: HTTP-ответ 404 сам по себе не отклоняет промис fetch(). Проверка response.ok и причины сетевых отказов разобраны в материале про Failed to fetch.
Проверь себя: что выведет функция без return
Определи порядок четырёх строк до запуска:
async function chooseAnswer() {
console.log(1)
await Promise.resolve(42)
console.log(2)
}
const result = chooseAnswer()
console.log(3)
result.then((value) => console.log(value)).catch(console.error)
Ответ:
1
3
2
undefined
1 печатается до паузы, 3 — после возврата управления в скрипт, 2 — после возобновления функции. Последнее значение — undefined: результат await нигде не сохранён и не возвращён.
Измени функцию так, чтобы последней строкой стало 42, а порядок 1, 3, 2 сохранился. Решение: присвой результат ожидания переменной answer, оставь console.log(2) и после него добавь return answer.
Дальше закрепи промисы и async/await на задачах курса «Современный JavaScript». Ошибки размещения await, асинхронные callback и ожидание нескольких результатов разобраны отдельно в статье про await вне async-функции.