Перейти к содержимому
Теория · Асинхронность · урок 7 из 12

async и await

В цепочке промисов каждый шаг живёт в своём колбэке, и код читается не сверху вниз. Слова async и await позволяют писать ожидание как обычные строки.

Снова представьте повара. В рецепте хлеба есть шаг «дождаться, пока подойдёт тесто». Повар не стоит над миской час: он кладёт закладку в рецепт и готовит другие заказы. Когда тесто подошло, он открывает рецепт на закладке и продолжает со следующего шага. Давайте посмотрим, как это выглядит в коде:

JavaScript
function wait(ms, value) {
    return new Promise((resolve) => {
        setTimeout(() => resolve(value), ms);
    });
}

async function main() {
    const name = await wait(100, "Ann");
    console.log(name);
}

main();
console.log("after main");
// after main
// Ann

Разберём пример по частям:

  1. Функция wait возвращает промис, который через ms миллисекунд выполнится со значением value.
  2. Внутри main строка с await ждёт этого и кладёт "Ann" в константу name.
  3. Следующая строка console.log(name) выполняется только потом и печатает Ann.

Поэтому код внутри main читается сверху вниз, как без ожидания. В одной функции await можно писать много раз: каждая такая строка ждёт свой промис по очереди.

Функция с async

Слово async перед function делает функцию асинхронной. Такая функция всегда возвращает промис, а значение после return становится значением этого промиса:

JavaScript
async function one() {
    return 1;
}

const p = one();
console.log(typeof p); // object
console.log(p instanceof Promise); // true
p.then((v) => console.log(v)); // 1

Что здесь происходит:

  • one() возвращает промис, а не число 1;
  • для промиса, как для массива, typeof даёт "object";
  • отличить промис от других объектов помогает instanceof Promise;
  • само число 1 достают через then или через await.

На собеседовании любят спрашивать, что вернёт вызов async-функции. Ответ: промис, даже если внутри написано return 1.

Что делает await

await ставит на паузу только свою async-функцию, пока промис справа от него не перестанет быть pending. Остальная программа в это время работает дальше. Это и есть закладка в рецепте: повар отложил один рецепт, а не всю кухню.

Поэтому в первом примере after main печатается раньше Ann. Когда main дошла до await, вызов main() вернул промис, и выполнилась следующая строка.

Даже если промис уже выполнен, продолжение функции ждёт, как колбэк then, пока закончится текущий код.

Здесь аналогия с поваром перестаёт работать. Повар может забыть, на чём остановился, а функция продолжает ровно со строки после await, и все её переменные на месте.

Отказ и try…catch

Если промис выполнен, await даёт его значение. Если промис отклонён, await бросает его причину, как throw. Такую ошибку ловят обычным try…catch:

JavaScript
async function load() {
    try {
        await Promise.reject(new Error("no network"));
        console.log("loaded");
    } catch (e) {
        console.log(e.message);
    }
}

load();
// no network

Promise.reject(reason) создаёт уже отклонённый промис. Строка loaded пропущена: после броска выполнение переходит в catch.

Если ошибку внутри async-функции не поймать, функция не падает на месте. Промис, который она вернула, становится rejected с этой ошибкой, и поймать её можно снаружи: load().catch((e) => …). Внутри функции try…catch обычно удобнее: обработка стоит рядом со строкой, которая ждёт.

Поэтому try…catch вокруг вызова без await такую ошибку не поймает. Сам вызов ничего не бросает: он сразу возвращает промис, а отказ этого промиса видят только catch и await.

Частая ошибка

await написали в функции без async:

JavaScript
function show() {
    const name = await wait(100, "Ann"); // ✗
    console.log(name);
}
TEXT
SyntaxError: await is only valid in async functions and the top level bodies of modules

Это SyntaxError, поэтому файл не запускается совсем, даже если show нигде не вызвана. Конец сообщения говорит о модулях — файлах с import и export. В курсе их нет, а в обычном файле await работает только внутри async-функции. Добавьте async перед function.

Главное

  • async-функция всегда возвращает промис, а значение после return становится его значением.
  • await ставит на паузу только свою функцию, пока промис не перестанет быть pending; остальная программа идёт дальше.
  • Если промис отклонён, await бросает его причину, и её ловят try…catch.
  • await работает только внутри async-функции.

Подпишись, чтобы пройти курс

Курс бесплатный. Войди через Telegram и подпишись на канал — откроются все уроки.

Войти другим способом