Menu

TypeScript async/await:类型、错误处理与并行调用

TypeScript 中 async 和 await 的类型:async 函数返回 Promise<T>,await 把它解包,错误用 try/catch 捕获,顶层 await 需要 ES 模块,以及逐个 await、并行 await 的区别和 forEach 的陷阱。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

TypeScript 中的 async 函数总是返回一个 Promise:如果函数体返回 string,它的类型就是 Promise<string>。在函数内部,await 把 Promise<T> 解包成 T。

运行时的行为(await 暂停了什么、事件循环)属于 JavaScript,在 JavaScript async/await 中讲解。TypeScript 负责的是输入和输出的类型。

async 函数的返回类型

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) 这个 bug 是真实存在的:Promise 对象永远为真值,所以它放行了访问。TypeScript 会把它报告为 TS2801。把这个 Promise 赋给 boolean 变量,或者读取 Promise 上不存在的属性,同样无法通过编译。但如果你忽略了调用的结果(save(user);),编译器发现不了;typescript-eslint 的 no-floating-promises 规则可以覆盖这种情况。

用 try/catch 处理错误

被拒绝的 Promise 会让 await 抛出异常,所以普通的 try/catch 就能用。在 strict 下,catch 中的变量是 unknown,读取 .message 之前要先收窄:

在 async 函数中 throw,会让它返回的 Promise 被拒绝,而不是在调用处直接抛出。更多模式,包括自定义错误类和用返回结果代替抛出异常,请看错误处理页面。

顶层 await

在任何函数之外使用 await,只在 ES 模块中有效。编译为 CommonJS 的文件(本页的示例就是这种情况,没有 "type": "module" 的 Node 项目也是)会拒绝它:

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

把代码包进一个 async 的 main 函数再调用它,就像本页每个示例那样。在 ES 模块项目中(package.json 中有 "type": "module",且 module 设为 node16 或 nodenext;或者使用打包工具时设为 module: "esnext"),可以使用顶层 await。

顺序执行与并行执行

每个 await 都会等它的 Promise 完成,下一行才开始。对于互不依赖的调用,先把它们全部启动,再用 Promise.all 一起 await:

在顺序执行的部分,b 不可能比 a 先完成,因为它还没开始。在并行执行的部分,d 先完成,总耗时大约等于最长的那次调用,而不是所有调用之和。Promise.all 仍然按输入顺序返回结果,类型是一个元组。

forEach 的陷阱

forEach 会忽略 async 回调返回的 Promise,所以没有任何东西在等这些工作完成:

for...of 加 await 会一次处理一个元素;Promise.all 配合 map 会并行执行并等待全部完成。forEach 两者都做不到,而且 TypeScript 不会发出警告,因为返回类型为 void 的回调也能接受返回 Promise 的函数。

用 for await 进行异步迭代

for await...of 遍历一个异步可迭代对象(比如异步生成器),并 await 每一个值:

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> 注解;如果不写注解,则由推断得出。

常见问题

TypeScript 中 async 函数的返回类型是什么?

永远是 Promise。返回 number 的 async 函数,返回类型是 Promise<number>;什么都不返回的则是 Promise<void>。写成 async function f(): number 会报错 TS1064,并提示你改成 Promise<number>。

TypeScript 中如何在顶层使用 await?

顶层 await 只能在 ES 模块中使用:module 设为 es2022、esnext、system、preserve,或者在 Node 视为 ESM 的文件中设为 node16/node18/node20/nodenext,并且 target 为 es2017 或更高。在 CommonJS 文件中会报错 TS1309。通用的解决办法是写一个 async 的 main 函数:async function main() { ... } main();。

TypeScript 中如何用 async/await 处理错误?

用 try/catch 包住 await。在 strict 下,捕获到的值类型是 unknown,所以要先收窄:if (e instanceof Error) console.log(e.message)。一个从未被 await 或捕获的拒绝会变成未处理的拒绝(unhandled rejection)。

TypeScript 中如何并行执行异步调用?

先启动所有 Promise,再一起 await:const [a, b] = await Promise.all([loadA(), loadB()])。写成 await loadA(); await loadB(); 会让它们一个接一个地执行。Promise.all 会在结果元组中保留每个结果的类型。

为什么 async 在 forEach 里不起作用?

forEach 调用回调后会忽略它的返回值,所以 async 回调返回的 Promise 永远不会被 await:循环立即结束,循环之后的代码在工作完成前就运行了。逐个处理用 for...of 加 await,并行处理用 await Promise.all(items.map(async (x) => ...))。

Coddy 编程语言插图

和 Coddy 一起学编程

开始