📚 TypeScript 教程系列
入门与配置 基础类型与变量声明 函数 流程控制与运算符 集合类型 异步编程与错误处理 (本文)接口与类 泛型与类型组合 高级类型 模块、装饰器与工程化 TypeScript 与 Bun 实战 TypeScript 与 React 实战 ⚠️ 来源声明 :本文内容参考自 菜鸟教程 TypeScript 教程 ,仅供学习交流,版权归原作者所有。 JavaScript 中大量操作都是异步的,例如网络请求、文件读取、定时器等。Promise 为这些异步操作提供了统一的编程接口,而 TypeScript 又通过泛型为 Promise 加上了精确的类型约束。本篇将把 Promise、async/await 与错误处理三部分串联起来,从创建、链式调用、组合方法,到同步风格的异步语法,再到自定义错误与 Result 类型,系统讲解 TypeScript 异步编程的完整图景。
Promise 异步编程 Promise 是 JavaScript 异步编程的基础,TypeScript 为其提供了完整的类型支持。通过泛型参数,可以精确指定 Promise 成功解决(resolve)时的值类型与失败拒绝(reject)时的原因类型。
为什么需要 Promise 在 JavaScript 中,许多操作都是异步的,比如网络请求、文件读取、定时器等。Promise 提供了统一的异步编程接口,使异步代码更易于编写和管理。TypeScript 的泛型则确保了 Promise 的类型安全。
概念说明: Promise 是一个代表异步操作最终结果的对象。它有三种状态:pending(进行中)、fulfilled(已成功)和 rejected(已失败)。
Promise 的工作流程 Promise 的生命周期可以用下面的流程表示:
flowchart LR
A["new Promise(executor)"] --> B["pending"]
B --> C["fulfilled ✓"]
B --> D["rejected ✗"]
C --> E["then() / finally()"]
D --> F["catch() / finally()"]一旦 Promise 从 pending 转为 fulfilled 或 rejected,就不可再改变状态。
创建 Promise 使用 Promise 构造函数,传入一个执行器(executor)函数即可创建 Promise。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 const promise = new Promise <string >(function (resolve, reject ) { const success = true ; if (success) { resolve ("成功!" ); } else { reject (new Error ("失败" )); } }); promise.then (function (value ) { console .log ("完成: " + value); }).catch (function (error ) { console .log ("错误: " + error.message ); });
运行结果:
泛型说明: Promise<T> 中的 T 是 Promise 成功解决时值的类型,这让 TypeScript 能够推断出返回类型。
then / catch / finally Promise 提供三个方法来处理异步结果:
方法 说明 then(onFulfilled, onRejected?)处理成功(或失败)的情况,返回一个新的 Promise catch(onRejected)处理失败的情况,等价于 then(undefined, onRejected) finally(onFinally)无论成功或失败都会执行,常用于清理资源,不接收值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 function task ( ): Promise <string > { return new Promise (function (resolve ) { setTimeout (function ( ) { resolve ("done" ); }, 100 ); }); }task () .then (function (value ) { console .log ("then: " + value); }) .catch (function (error ) { console .log ("catch: " + error.message ); }) .finally (function ( ) { console .log ("finally: 清理资源" ); });
运行结果:
1 2 then: donefinally: 清理资源
注意: finally 不接收任何参数,也无法改变后续 Promise 的值;它主要用来执行清理逻辑,例如关闭文件句柄、隐藏 loading 状态等。
现代写法(TS 5.2+): 对于实现了 Symbol.dispose/Symbol.asyncDispose 的资源,可用 using(同步)或 await using(异步,如数据库连接、文件句柄)声明,作用域结束时会自动释放,比手动 finally 更简洁安全。
1 2 3 4 5 6 7 8 9 10 11 12 13 declare class DbConnection { constructor (url : string ); query (sql : string , ...params : unknown []): Promise <unknown >; [Symbol .asyncDispose ](): Promise <void >; }async function queryUser (id : number ): Promise <unknown > { await using conn = new DbConnection ("/api/db" ); return await conn.query ("SELECT * FROM users WHERE id = ?" , id); }
Promise 链式调用 then 和 catch 方法都会返回新的 Promise,因此可以链式调用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 const promise = Promise .resolve (1 ) .then (function (n ) { return n * 2 ; }) .then (function (n ) { return n + 10 ; }) .then (function (n ) { console .log ("最终结果: " + n); return n; });console .log ("Promise 链: " + promise);
运行结果:
1 2 最终结果: 12 Promise 链: [ object Promise]
链式调用: 每个 then 都返回一个新的 Promise,从而可以让多个异步操作按顺序执行。
如果 then 的回调返回的是一个 Promise,下一个 then 会等待该 Promise 解决后再执行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 function delay (ms : number ): Promise <string > { return new Promise (function (resolve ) { setTimeout (function ( ) { resolve ("等待了 " + ms + "ms" ); }, ms); }); }delay (100 ) .then (function (result ) { console .log (result); return delay (50 ); }) .then (function (result ) { console .log (result); });
运行结果:
Promise.all Promise.all 等待所有 Promise 都完成,返回包含所有结果的数组。
1 2 3 4 5 6 7 8 9 10 11 12 const p1 = Promise .resolve (1 );const p2 = Promise .resolve (2 );const p3 = Promise .resolve (3 );Promise .all ([p1, p2, p3]).then (function (results ) { console .log ("全部完成: " + results); console .log ("总和: " + results.reduce (function (a, b ) { return a + b; }, 0 )); });
运行结果:
注意: 只要有一个 Promise 失败,Promise.all 就会立即拒绝,不再等待其他 Promise。
Promise.race Promise.race 返回最先完成(无论成功或失败)的 Promise 的结果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const p1 = new Promise (function (resolve ) { setTimeout (function ( ) { resolve ("p1" ); }, 100 ); });const p2 = new Promise (function (resolve ) { setTimeout (function ( ) { resolve ("p2" ); }, 50 ); });const p3 = new Promise (function (resolve ) { setTimeout (function ( ) { resolve ("p3" ); }, 30 ); });Promise .race ([p1, p2, p3]).then (function (value ) { console .log ("最先完成: " + value); });
运行结果:
应用场景: Promise.race 常用于实现超时功能——把一个耗时操作和一个超时 Promise 放在一起竞速。
Promise.allSettled Promise.allSettled 等待所有 Promise 都结束(无论成功或失败),并返回每个 Promise 的状态与结果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 const p1 = Promise .resolve ("成功" );const p2 = Promise .reject (new Error ("失败" ));const p3 = Promise .resolve ("完成" );Promise .allSettled ([p1, p2, p3]).then (function (results ) { results.forEach (function (result, index ) { if (result.status === "fulfilled" ) { console .log ("Promise " + index + ": " + result.value ); } else { console .log ("Promise " + index + ": " + result.reason .message ); } }); });
运行结果:
1 2 3 Promise 0 : 成功Promise 1 : 失败Promise 2 : 完成
区别: Promise.all 在第一个失败时立即停止;Promise.allSettled 无论成功失败都会等待所有 Promise 结束。
Promise.any Promise.any(ES2021)只要有一个 Promise 成功就立即解决为该值;只有当全部 Promise 都失败时,才会拒绝并抛出 AggregateError,其 errors 属性包含所有拒绝原因。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const mirrors = [ fetch ("https://cdn-a.example.com/data" ).then (function (r ) { return r.json (); }), fetch ("https://cdn-b.example.com/data" ).then (function (r ) { return r.json (); }), fetch ("https://cdn-c.example.com/data" ).then (function (r ) { return r.json (); }), ];Promise .any (mirrors) .then (function (data ) { console .log ("最快可用源:" , data); }) .catch (function (err ) { console .log ("全部失败:" , err.errors ); });
与 Promise.race 的区别: race 会采纳最先结束的结果(含失败);any 忽略失败,只要有一个成功就返回,全部失败才拒绝。
下表对四种组合方法做一对比:
方法 行为 失败处理 Promise.all等待全部完成 任一失败即整体拒绝 Promise.race返回最先结束的结果 最先失败即整体拒绝 Promise.allSettled等待全部结束 不拒绝,返回每个状态 Promise.any任一成功即返回 全部失败时抛出 AggregateError
Promise 类型注解 TypeScript 的泛型允许为 Promise 做精确的类型声明。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 function getUser ( ): Promise <{ name : string ; age : number }> { return Promise .resolve ({ name : "Alice" , age : 25 }); }async function main ( ) { const user = await getUser (); console .log ("用户: " + JSON .stringify (user)); }main ();
类型推断: TypeScript 会根据泛型参数自动推断 Promise 的返回类型,即使在 async/await 代码中也能获得完整的类型提示。
Promise 使用注意事项 泛型参数: 始终为 Promise 指定泛型参数,以明确返回类型。错误处理: 记得使用 catch 处理 Promise 的拒绝情况。all vs allSettled: 需要全部结果时用 allSettled,希望快速失败时用 all。async/await: 现代代码推荐使用 async/await,语法更简洁。最佳实践: 优先使用 async/await 语法——它本质上仍基于 Promise,但读起来像同步代码。
async/await async/await 是 ES2017 引入的异步编程语法糖,让异步代码看起来像同步代码。它仍然建立在 Promise 之上,但显著提升了可读性与可维护性。
async/await 相比原生 Promise 具有以下优势:
代码更简洁、更易读 同步代码风格 更好的错误堆栈 易于调试,可用 try/catch 处理 async 函数 使用 async 关键字声明异步函数。async 函数会自动返回 Promise。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 async function greet ( ): Promise <string > { return "Hello, World!" ; }greet ().then (function (result ) { console .log ("结果: " + result); });async function getData ( ) { return { name : "Alice" , age : 25 }; }getData ().then (function (data ) { console .log ("数据: " + JSON .stringify (data)); });
运行结果:
1 2 结果: Hello, World! 数据: { "name" : "Alice" , "age" : 25 }
在 async 函数中,return 的值会被自动包装成 Promise.resolve(value);如果抛出异常,则会被包装成 Promise.reject(error)。
await 关键字 await 用于等待一个 Promise 完成并获取其结果。await 只能在 async 函数内部使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 function delay (ms : number ): Promise <string > { return new Promise (function (resolve ) { setTimeout (function ( ) { resolve ("完成!" ); }, ms); }); }async function main ( ) { console .log ("开始..." ); const result = await delay (100 ); console .log ("结果: " + result); console .log ("结束" ); }main ();
运行结果:
说明: await 会暂停 async 函数的执行,直到 Promise 解决,但不会阻塞主线程;期间事件循环仍可处理其他任务。
并行执行 使用 Promise.all 可以让多个异步操作并行执行,而不必串行等待。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 function fetchUser (id : number ): Promise <{ id : number ; name : string }> { return Promise .resolve ({ id : id, name : "User" + id }); }async function main ( ) { console .time ("串行" ); const user1 = await fetchUser (1 ); const user2 = await fetchUser (2 ); console .log ("串行完成: " + user1.name + ", " + user2.name ); console .timeEnd ("串行" ); console .time ("并行" ); const results = await Promise .all ([fetchUser (1 ), fetchUser (2 )]); console .log ("并行完成: " + results[0 ].name + ", " + results[1 ].name ); console .timeEnd ("并行" ); }main ();
运行结果:
1 2 串行完成: User1, User2 并行完成: User1, User2
提示: 当多个异步操作之间没有依赖关系时,应使用 Promise.all 并行执行,以显著缩短总耗时。
顶层 await 从 ES2022 起,await 可以在模块的顶层直接使用,而不必包裹在 async 函数中。TypeScript 5.x 在 module 设为 es2022/esnext/preserve(5.4 新增)/nodenext 时支持顶层 await;现代项目推荐 --module preserve 配合 moduleResolution: "bundler",或直接用 nodenext。target 与顶层 await 无关,只需支持 async 语法即可。
1 2 3 4 5 const config = await fetch ("/api/config" ).then (function (r ) { return r.json (); });console .log ("配置加载完成:" , config);export { config };
注意: 顶层 await 会阻塞依赖该模块的所有模块加载,因此在库代码中应谨慎使用,避免影响启动性能。
异步迭代 for await…of for...of 用于遍历同步的可迭代对象;当数据源是异步产生(如分页接口、Node.js 可读流)时,需要用 for await...of 遍历异步可迭代对象 (实现了 Symbol.asyncIterator 的对象)。每次迭代都会 await,按顺序拿到结果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 async function * fetchPages (url : string ) { let page = 1 ; while (true ) { const res = await fetch (`${url} ?page=${page} ` ); const data : number [] = await res.json (); if (data.length === 0 ) break ; yield data; page++; } }async function main ( ) { for await (const page of fetchPages ("/api/items" )) { console .log (page); } }
异步生成器(async function*)天然返回异步可迭代对象,是与 for await...of 配合最常见的方式;也可以手动实现 [Symbol.asyncIterator],返回一个 next() 返回 Promise<{ done: boolean; value: T }> 的对象。
⚠️ for await...of 必须在 async 函数或顶层 await 上下文中使用;它会串行等待每一次迭代,若各步之间无依赖关系且需要并行时,仍应改用 Promise.all。
async/await 与 Promise 对比 对比项 Promise(then 链) async/await 语法风格 链式调用,回调嵌套 同步风格,线性书写 错误处理 .catch()try/catch调试 断点不易跟入回调 可直接在 await 行打断点 错误堆栈 跨 then 较模糊 指向具体 await 行 并发控制 Promise.allawait Promise.all
建议: async/await 与 Promise 并非二选一——async/await 底层就是 Promise,组合方法(Promise.all 等)依然需要在 async 函数中通过 await 使用。
错误处理 错误处理是保证程序健壮性的重要环节。TypeScript 提供了强大的类型系统,帮助开发者更好地处理和预防错误。本节介绍 TypeScript 中常见的错误处理模式与最佳实践。
错误处理主要有两种思路:异常处理 (try-catch)通过抛出异常来传递错误;返回值处理 (Result 类型)通过返回值携带错误信息。
为什么需要良好的错误处理 任何程序都可能遇到错误情况,例如网络请求失败、文件缺失、用户输入不合法等。良好的错误处理可以防止程序崩溃,提供友好的错误信息,并帮助开发者定位问题。TypeScript 的类型系统能在编译期捕获潜在问题,减少运行时错误。
try/catch 捕获错误 try/catch 是最基础的错误处理方式,用于包裹可能抛出异常的代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 function divide (a : number , b : number ): number { if (b === 0 ) { throw new Error ("除数不能为零" ); } return a / b; }try { const result = divide (10 , 0 ); console .log ("结果: " + result); } catch (error) { if (error instanceof Error ) { console .log ("捕获错误: " + error.message ); } else { console .log ("未知错误: " + error); } }
运行结果:
提示: 在 strict 模式下,catch 子句的变量默认类型为 unknown(由 useUnknownInCatchVariables 选项控制),使用前应通过类型守卫(如 instanceof)收窄类型,避免不安全的 any 假设。
内置 Error 类型 JavaScript 内置了多种错误类型,它们都继承自 Error,可在不同场景下抛出:
错误类型 说明 Error通用错误基类 TypeError类型不正确时抛出 RangeError数值超出合法范围时抛出 SyntaxError语法错误(如 eval 解析失败) ReferenceError引用不存在的变量时抛出 URIErrorURI 处理函数使用不当时抛出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 function parseValue (value : unknown ): number { if (typeof value !== "number" ) { throw new TypeError ("期望数字,但收到 " + typeof value); } if (value < 0 || value > 100 ) { throw new RangeError ("数值必须在 0-100 之间" ); } return value; }try { parseValue ("abc" ); } catch (error) { if (error instanceof TypeError ) { console .log ("类型错误: " + error.message ); } else if (error instanceof RangeError ) { console .log ("范围错误: " + error.message ); } }
运行结果:
自定义错误类型 通过扩展 Error 类,可以创建携带额外错误信息的自定义错误类型,使错误处理更加精确、结构化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 class AppError extends Error { code : string ; constructor (message : string , code : string ) { super (message); this .name = "AppError" ; this .code = code; } }function divide (a : number , b : number ): number { if (b === 0 ) { throw new AppError ("Cannot divide by zero" , "DIVIDE_BY_ZERO" ); } return a / b; }try { const result = divide (10 , 0 ); } catch (error) { if (error instanceof AppError ) { console .log ("应用错误: " + error.message + ", 代码: " + error.code ); } else { console .log ("未知错误: " + error); } }
运行结果:
1 应用错误: Cannot divide by zero , 代码: DIVIDE_BY_ZERO
错误码: 为错误添加错误码,有助于程序更精确地针对不同错误类型做处理。
Result 类型避免异常 另一种错误处理思路是使用 Result 类型——通过返回值携带错误信息,而不是抛出异常。这种模式在函数式编程中很常见。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 type Result <T, E = Error > = | { ok : true ; value : T } | { ok : false ; error : E };function safeDivide (a : number , b : number ): Result <number , string > { if (b === 0 ) { return { ok : false , error : "Cannot divide by zero" }; } return { ok : true , value : a / b }; }const result = safeDivide (10 , 2 );if (result.ok ) { console .log ("结果: " + result.value ); } else { console .log ("错误: " + result.error ); }
运行结果:
优势: Result 类型让错误处理变得显式——调用方必须处理潜在的错误,而不能简单地忽略它。TypeScript 的类型系统会在编译期强制检查 ok 分支,访问 value 或 error 时也能获得类型收窄。
TS 5.4 闭包中保留窄化: Result 类型依赖判别联合收窄——if (result.ok) 之后 result.value 即可安全访问。TS 5.4 起,这种收窄在闭包(回调、then、setTimeout 等)中也会保留;此前进入闭包后收窄会丢失,往往需要再次断言。
1 2 3 4 5 6 7 8 const result = safeDivide (10 , 0 );if (!result.ok ) { setTimeout (function ( ) { console .log ("异步记录错误:" , result.error ); }, 0 ); }
async 函数错误处理 在异步函数中,错误处理尤为重要。可以使用 try-catch 或 Result 类型来处理异步操作中的错误。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 interface User { id : number ; name : string ; }type Result <T, E = Error > = | { ok : true ; value : T } | { ok : false ; error : E };async function fetchUser (id : number ): Promise <Result <User , Error >> { try { const response = await fetch ("/api/users/" + id); const user = await response.json (); return { ok : true , value : user }; } catch (error) { return { ok : false , error : error instanceof Error ? error : new Error (String (error)) }; } }async function main ( ) { const result = await fetchUser (1 ); if (result.ok ) { console .log ("用户: " + JSON .stringify (result.value )); } else { console .log ("错误: " + result.error .message ); } }main ();
运行结果:
1 用户: { "id" : 1 , "name" : "Alice" }
提示: 在 async 函数中,try-catch 会捕获 await 表达式抛出的任何错误。
通用错误处理封装 可以创建一个通用的错误处理函数,简化异步代码中的错误处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 type Result <T, E = Error > = | { ok : true ; value : T } | { ok : false ; error : E };async function withErrorHandling<T>( fn : () => Promise <T> ): Promise <Result <T, Error >> { try { const data = await fn (); return { ok : true , value : data }; } catch (error) { return { ok : false , error : error instanceof Error ? error : new Error (String (error)) }; } }async function main ( ) { const result = await withErrorHandling (async function ( ) { const response = await fetch ("/api/data" ); return response.json (); }); if (result.ok ) { console .log ("数据: " + JSON .stringify (result.value )); } else { console .error ("错误:" , result.error ); } }main ();
最佳实践: 把通用的错误处理逻辑封装起来,可以减少代码重复,提升可维护性。
抛出与捕获策略 错误处理不仅仅是语法问题,更涉及设计策略。以下原则有助于在抛出与捕获之间取得平衡:
在底层抛出,在高层捕获: 底层函数只负责抛出具体错误,由调用链上层决定如何处理(记录日志、回退、上报)。不要吞掉错误: 避免空的 catch 块;若确实要忽略,应注释说明原因。转换错误层级: 在模块边界把底层错误转换为领域错误(如把 TypeError 转成 AppError),避免内部实现泄漏。保留原始堆栈: 转换错误时通过 cause 字段(ES2022)保留原始错误,便于追踪。1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 class ServiceError extends Error { code : string ; cause ?: unknown ; constructor (message : string , code : string , cause ?: unknown ) { super (message); this .name = "ServiceError" ; this .code = code; if (cause !== undefined ) { this .cause = cause; } } }async function loadProfile (id : number ) { try { const res = await fetch ("/api/profile/" + id); if (!res.ok ) { throw new ServiceError ("请求失败" , "HTTP_" + res.status ); } return await res.json (); } catch (error) { throw new ServiceError ("加载用户资料失败" , "PROFILE_LOAD_FAILED" , error); } }
错误处理注意事项 不要忽略错误: 不要用空的 catch 块吞掉错误。明确错误类型: 尽可能使用具体的错误类型,而不是通用的 Error。错误边界: 在整个应用中建立统一的错误处理机制。不要过度使用异常: 对于可预见的错误,优先使用返回值而非抛出异常。建议: 根据上下文选择处理方式——程序错误用异常,业务错误用 Result 类型。
总结 Promise 是 TypeScript 异步编程的核心,async/await 让异步代码具备同步风格的可读性,而合理的错误处理则是程序健壮性的基石。三者层层递进,构成了现代 TypeScript 异步代码的完整工具链。
Promise: 异步操作容器,有 pending、fulfilled、rejected 三种状态。then/catch/finally: 链式处理异步结果与清理资源。Promise.all / race / allSettled / any: 组合多个 Promise,分别对应全部完成、竞速、全部结束、任一成功。async/await: 基于 Promise 的语法糖,代码更简洁、更易调试。顶层 await: 模块顶层直接 await,简化模块初始化场景。自定义错误: 扩展 Error 类,添加错误码等信息。Result 类型: 通过返回值处理错误,避免异常。错误封装: 创建通用错误处理函数,统一错误边界。最佳实践: 根据具体场景选择合适的错误处理方式,在可读性与健壮性之间取得平衡。