📚 TypeScript 教程系列
- 入门与配置
- 基础类型与变量声明
- 函数
- 流程控制与运算符
- 集合类型
- 异步编程与错误处理(本文)
- 接口与类
- 泛型与类型组合
- 高级类型
- 模块、装饰器与工程化
⚠️ 来源声明:本文内容参考自 菜鸟教程 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 的生命周期可以用下面的流程表示:
1
| 创建 Promise → new Promise((resolve, reject) => {...}) → pending(等待中)→ fulfilled(✓)或 rejected(✗)→ then() · 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
|
var promise = new Promise<string>(function (resolve, reject) { var 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: done finally: 清理资源
|
注意: finally 不接收任何参数,也无法改变后续 Promise 的值;它主要用来执行清理逻辑,例如关闭文件句柄、隐藏 loading 状态等。
Promise 链式调用
then 和 catch 方法都会返回新的 Promise,因此可以链式调用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| var 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
| var p1 = Promise.resolve(1); var p2 = Promise.resolve(2); var 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
| var p1 = new Promise(function (resolve) { setTimeout(function () { resolve("p1"); }, 100); }); var p2 = new Promise(function (resolve) { setTimeout(function () { resolve("p2"); }, 50); }); var 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
| var p1 = Promise.resolve("成功"); var p2 = Promise.reject(new Error("失败")); var 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.all | 等待全部完成 | 任一失败即整体拒绝 |
Promise.race | 返回最先结束的结果 | 最先失败即整体拒绝 |
Promise.allSettled | 等待全部结束 | 不拒绝,返回每个状态 |
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() { var 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("开始..."); var result = await delay(100); console.log("结果: " + result); console.log("结束"); }
main();
|
运行结果:
说明: await 会暂停 async 函数的执行,直到 Promise 解决,但不会阻塞主线程;期间事件循环仍可处理其他任务。
错误处理
在 async 函数中,使用 try/catch 处理异步错误。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| function mayFail(shouldFail: boolean): Promise<string> { return new Promise(function (resolve, reject) { if (shouldFail) { reject(new Error("操作失败")); } else { resolve("操作成功"); } }); }
async function handleError() { try { var result = await mayFail(true); console.log("结果: " + result); } catch (error) { console.log("捕获错误: " + error.message); } }
handleError();
|
运行结果:
并行执行
使用 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("串行"); var user1 = await fetchUser(1); var user2 = await fetchUser(2); console.log("串行完成: " + user1.name + ", " + user2.name); console.timeEnd("串行");
console.time("并行"); var 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 在 module 配置为 ESNext 且 target 不低于 ES2017 时支持该特性。
1 2 3 4 5
| var config = await fetch("/api/config").then(function (r) { return r.json(); }); console.log("配置加载完成:", config);
export { config };
|
注意: 顶层 await 会阻塞依赖该模块的所有模块加载,因此在库代码中应谨慎使用,避免影响启动性能。
async/await 与 Promise 对比
| 对比项 | Promise(then 链) | async/await |
|---|
| 语法风格 | 链式调用,回调嵌套 | 同步风格,线性书写 |
| 错误处理 | .catch() | try/catch |
| 调试 | 断点不易跟入回调 | 可直接在 await 行打断点 |
| 错误堆栈 | 跨 then 较模糊 | 指向具体 await 行 |
| 并发控制 | Promise.all | await 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 { var result = divide(10, 0); console.log("结果: " + result); } catch (error) { if (error instanceof Error) { console.log("捕获错误: " + error.message); } else { console.log("未知错误: " + error); } }
|
运行结果:
提示: TypeScript 4.x 起,catch 子句的变量默认类型为 unknown,使用前应通过类型守卫(如 instanceof)收窄类型,避免不安全的 any 假设。
内置 Error 类型
JavaScript 内置了多种错误类型,它们都继承自 Error,可在不同场景下抛出:
| 错误类型 | 说明 |
|---|
Error | 通用错误基类 |
TypeError | 类型不正确时抛出 |
RangeError | 数值超出合法范围时抛出 |
SyntaxError | 语法错误(如 eval 解析失败) |
ReferenceError | 引用不存在的变量时抛出 |
URIError | URI 处理函数使用不当时抛出 |
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 { var 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 }; }
var result = safeDivide(10, 2);
if (result.ok) { console.log("结果: " + result.value); } else { console.log("错误: " + result.error); }
|
运行结果:
优势: Result 类型让错误处理变得显式——调用方必须处理潜在的错误,而不能简单地忽略它。TypeScript 的类型系统会在编译期强制检查 ok 分支,访问 value 或 error 时也能获得类型收窄。
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 { var response = await fetch("/api/users/" + id); var user = await response.json(); return { ok: true, value: user }; } catch (error) { return { ok: false, error: error as Error }; } }
async function main() { var 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
| 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 { var data = await fn(); return { ok: true, value: data }; } catch (error) { return { ok: false, error: error as Error }; } }
var result = await withErrorHandling(async function () { var response = await fetch("/api/data"); return response.json(); });
if (result.ok) { console.log("数据: " + JSON.stringify(result.value)); } else { console.error("错误:", result.error); }
|
最佳实践: 把通用的错误处理逻辑封装起来,可以减少代码重复,提升可维护性。
抛出与捕获策略
错误处理不仅仅是语法问题,更涉及设计策略。以下原则有助于在抛出与捕获之间取得平衡:
- 在底层抛出,在高层捕获: 底层函数只负责抛出具体错误,由调用链上层决定如何处理(记录日志、回退、上报)。
- 不要吞掉错误: 避免空的
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 { var 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: 组合多个 Promise,分别对应全部完成、竞速、全部结束。
- async/await: 基于 Promise 的语法糖,代码更简洁、更易调试。
- 顶层 await: 模块顶层直接 await,简化模块初始化场景。
- 自定义错误: 扩展 Error 类,添加错误码等信息。
- Result 类型: 通过返回值处理错误,避免异常。
- 错误封装: 创建通用错误处理函数,统一错误边界。
最佳实践: 根据具体场景选择合适的错误处理方式,在可读性与健壮性之间取得平衡。