Promise и async/await в JavaScript: порядок выполнения и ошибки

JavaScript Автор: Среда и версия: Node.js 20.20.2 и 24.21.0, Linux; примеры и упражнения выполнены отдельно, вывод сверён
содержание

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
  1. Скрипт печатает A, затем вызывает showAnswer(). Её начало выполняется сразу и печатает B.
  2. На await функция приостанавливается. Скрипт получает её промис, печатает C и подключает обработчик .then().
  3. После синхронного кода возобновляется showAnswer(): получает 42, печатает D 42 и завершается. Затем обработчик её промиса печатает E.
JavaScript / 01

Готовое значение не отменяет паузу на await

После A и B выполняется C, затем D 42 и E Первый этап: синхронный код печатает A и B до await. Второй: пока showAnswer приостановлена, вызывающий код печатает C. Третий: продолжение после await печатает D 42, затем обработчик завершённого промиса печатает E. 01 / до await 02 / вызов вернулся 03 / продолжение A B C D 42 E Начало скрипта и вызов showAnswer() Скрипт идёт дальше Результат await, затем обработчик .then() Promise.resolve(42): значение готово ещё до паузы
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 и не выбрасывает новую ошибку.

JavaScript / 02

Отказ промиса становится исключением на await

Ошибка из readName попадает в catch функции showName readName выбрасывает Error с сообщением «Имя не указано». Её промис отклоняется. await выбрасывает эту ошибку внутри try. catch печатает сообщение «Ошибка: Имя не указано». После обработчика выполняется вывод «Форма открыта». 01 / источник ошибки readName(): throw new Error(…) Promise: rejected Причина отказа: Error: Имя не указано 02 / ожидание внутри try await readName() Выбрасывает полученную ошибку 03 / обработка и продолжение catch (error) Вывод: Ошибка: Имя не указано console.log('Форма открыта') Вывод: Форма открыта
Сообщение «Ошибка: Имя не указано» печатает 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-функции.

Источники