Menu

Async/await в TypeScript: типы, ошибки и параллельные вызовы

Как типизируются async и await в TypeScript: функция async возвращает Promise<T>, await его разворачивает, ошибки ловятся через try/catch, await верхнего уровня требует модуля ES, а также разница между ожиданием по одному, параллельным ожиданием и ловушкой forEach.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

Функция async в TypeScript всегда возвращает промис: если её тело возвращает string, её тип Promise<string>. Внутри неё await разворачивает Promise<T> в T.

Поведение во время выполнения (что приостанавливает await, цикл событий) это JavaScript, описанный в разделе async/await в JavaScript. Часть TypeScript это типы на входе и выходе.

Типы возврата асинхронных функций

Объявленный тип возврата функции async должен быть Promise<...>, хотя тело возвращает обычное значение. Если его не указать, TypeScript выведет его сам.

Запись async function count(): number это ошибка TS1064 (The return type of an async function or method must be the global Promise<T> type. Did you mean to write 'Promise<number>'?). Служебный тип Awaited<T> даёт развёрнутый тип: Awaited<ReturnType<typeof count>> это number.

Забытый await

Пропущенный await оставляет у вас Promise<T> вместо T. TypeScript ловит большинство таких случаев, потому что типы перестают совпадать:

Ошибка с if (ok) настоящая: объект промиса всегда истинен, поэтому доступ был предоставлен. TypeScript сообщает о ней как TS2801. Присваивание промиса переменной boolean или чтение свойства, которого у промиса нет, тоже не скомпилировалось бы. Вызов, результат которого игнорируется (save(user);), не ловится; для этого есть правило typescript-eslint no-floating-promises.

Обработка ошибок через try/catch

Отклонённый промис заставляет await выбросить ошибку, поэтому обычный try/catch работает. При strict переменная в catch имеет тип unknown, и её нужно сузить, прежде чем читать .message:

throw внутри функции async отклоняет её промис, а не выбрасывает ошибку в месте вызова. Больше приёмов, включая собственные классы ошибок и возврат результатов вместо выбрасывания, есть на странице обработка ошибок.

await верхнего уровня

await вне любой функции работает только в модуле ES. Файл, скомпилированный как CommonJS (так обстоит дело с этими примерами и с проектами Node без "type": "module"), его отвергает:

index.ts(2,14): error TS1309: The current file is a CommonJS module and cannot use 'await' at the top level.

Оберните код в async-функцию main и вызовите её, как делает каждый пример на этой странице. В проекте на модулях ES ("type": "module" в package.json с module, равным node16 или nodenext, или module: "esnext" для бандлера) await верхнего уровня разрешён.

По очереди и параллельно

Каждый await ждёт свой промис, прежде чем начнётся следующая строка. Для независимых вызовов сначала запустите их все, а затем дождитесь вместе через Promise.all:

В последовательной половине b не может завершиться раньше a, потому что ещё не запущен. В параллельной половине первым завершается d, а общее время примерно равно самому долгому вызову, а не сумме. Promise.all всё равно возвращает результаты в порядке входов, типизированные как кортеж.

Ловушка forEach

forEach игнорирует промис, который возвращает async-колбэк, поэтому работу никто не ждёт:

for...of с await обрабатывает элементы по одному; Promise.all с map запускает их параллельно и ждёт все. forEach не делает ни того, ни другого, и TypeScript не предупреждает, потому что колбэк с типом возврата void принимает и тот, что возвращает промис.

Асинхронная итерация через for await

for await...of перебирает асинхронный итерируемый объект, например асинхронный генератор, дожидаясь каждого значения:

async function* pages(total: number): AsyncGenerator<string[]> {
    for (let page = 1; page <= total; page++) {
        await new Promise((r) => setTimeout(r, 10));
        yield [`item ${page}a`, `item ${page}b`];
    }
}

async function main() {
    for await (const batch of pages(3)) {
        console.log(batch.join(", ")); // batch: string[]
    }
}
main();

Тип элемента берётся из аннотации генератора AsyncGenerator<T> или выводится, если её не указать.

Часто задаваемые вопросы

Какой тип возврата у async-функции в TypeScript?

Всегда промис. Async-функция, которая возвращает number, имеет тип возврата Promise<number>, а та, что ничего не возвращает, Promise<void>. Запись async function f(): number это ошибка TS1064, которая предлагает Promise<number>.

Как использовать await на верхнем уровне в TypeScript?

await верхнего уровня работает только в модуле ES, когда module равен es2022, esnext, system, preserve или node16/node18/node20/nodenext в файле, который Node считает ESM, а target не ниже es2017. В файле CommonJS это ошибка TS1309. Переносимое решение это async-функция main: async function main() { ... } main();.

Как обрабатывать ошибки с async/await в TypeScript?

Оберните await в try/catch. При strict пойманное значение имеет тип unknown, поэтому сначала сузьте его: if (e instanceof Error) console.log(e.message). Отклонение, которое никто не ждёт и не ловит, становится необработанным.

Как запускать асинхронные вызовы параллельно в TypeScript?

Сначала запустите все промисы, затем дождитесь их вместе: const [a, b] = await Promise.all([loadA(), loadB()]). Запись await loadA(); await loadB(); выполняет их один за другим. Promise.all сохраняет тип каждого результата в итоговом кортеже.

Почему async не работает внутри forEach?

forEach вызывает колбэк и игнорирует то, что он возвращает, поэтому промисы из async-колбэка никто не ждёт: цикл завершается сразу, и код после него выполняется до окончания работы. Используйте for...of с await для работы по очереди или await Promise.all(items.map(async (x) => ...)) для параллельной работы.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ