📚 TypeScript 教程系列

  1. 入门与配置
  2. 基础类型与变量声明
  3. 函数
  4. 流程控制与运算符
  5. 集合类型
  6. 异步编程与错误处理(本文)
  7. 接口与类
  8. 泛型与类型组合
  9. 高级类型
  10. 模块、装饰器与工程化

⚠️ 来源声明:本文内容参考自 菜鸟教程 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
// 创建 Promise,使用泛型指定解决值的类型
// Promise<string> 表示成功时返回字符串
var promise = new Promise<string>(function (resolve, reject) {
var success = true;
if (success) {
// 调用 resolve 表示操作成功,传入结果值
resolve("成功!");
} else {
// 调用 reject 表示操作失败,传入错误
reject(new Error("失败"));
}
});

// 使用 then 处理成功情况
promise.then(function (value) {
console.log("完成: " + value);
}).catch(function (error) {
// 使用 catch 处理失败情况
console.log("错误: " + error.message);
});

运行结果:

1
完成: 成功!

泛型说明: 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 链式调用

thencatch 方法都会返回新的 Promise,因此可以链式调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 链式调用:每个 then 返回新的值,被下一个 then 接收
var promise = Promise.resolve(1)
.then(function (n) {
// 第一个 then,n = 1
return n * 2; // 返回 2
})
.then(function (n) {
// 第二个 then,n = 2
return n + 10; // 返回 12
})
.then(function (n) {
// 第三个 then,n = 12
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); // 返回新的 Promise
})
.then(function (result) {
console.log(result);
});

运行结果:

1
2
等待了 100ms
等待了 50ms

Promise.all

Promise.all 等待所有 Promise 都完成,返回包含所有结果的数组。

1
2
3
4
5
6
7
8
9
10
11
12
// 创建三个 Promise
var p1 = Promise.resolve(1);
var p2 = Promise.resolve(2);
var p3 = Promise.resolve(3);

// Promise.all 等待所有 Promise 完成
// 返回一个数组,包含所有 Promise 的结果
Promise.all([p1, p2, p3]).then(function (results) {
console.log("全部完成: " + results);
// 计算总和
console.log("总和: " + results.reduce(function (a, b) { return a + b; }, 0));
});

运行结果:

1
2
全部完成: 1,2,3
总和: 6

注意: 只要有一个 Promise 失败,Promise.all 就会立即拒绝,不再等待其他 Promise。

Promise.race

Promise.race 返回最先完成(无论成功或失败)的 Promise 的结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 创建三个不同延迟的 Promise
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 返回最先完成的 Promise 的结果
Promise.race([p1, p2, p3]).then(function (value) {
console.log("最先完成: " + value);
});

运行结果:

1
最先完成: p3

应用场景: 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
// 创建三个 Promise,其中一个会失败
var p1 = Promise.resolve("成功");
var p2 = Promise.reject(new Error("失败"));
var p3 = Promise.resolve("完成");

// Promise.allSettled 等待所有 Promise 结束
// 返回每个 Promise 的状态和值/reason
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
// 定义返回 Promise 的函数
// Promise<{ name: string; age: number }> 指定了返回的用户对象类型
function getUser(): Promise<{ name: string; age: number }> {
return Promise.resolve({ name: "Alice", age: 25 });
}

// async 函数:隐式返回 Promise
async function main() {
// await 会自动推断 user 的类型
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 函数自动返回 Promise
async function greet(): Promise<string> {
return "Hello, World!";
}

greet().then(function (result) {
console.log("结果: " + result);
});

// 异步函数返回 Promise
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();

运行结果:

1
2
3
开始...
结果: 完成!
结束

说明: 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();

运行结果:

1
捕获错误: 操作失败

并行执行

使用 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
// 顶层 await:模块加载时会等待该 Promise 完成
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.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 {
var result = divide(10, 0);
console.log("结果: " + result);
} catch (error) {
if (error instanceof Error) {
console.log("捕获错误: " + error.message);
} else {
console.log("未知错误: " + error);
}
}

运行结果:

1
捕获错误: 除数不能为零

提示: TypeScript 4.x 起,catch 子句的变量默认类型为 unknown,使用前应通过类型守卫(如 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);
}
}

运行结果:

1
类型错误: 期望数字,但收到 string

自定义错误类型

通过扩展 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
// 定义应用程序错误类
// 扩展内置 Error 类,添加错误码
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-catch 捕获错误
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
// 定义 Result 类型,使用联合类型
// 成功时包含 ok: true 和值,失败时包含 ok: false 和错误
type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E };

// 使用 Result 类型的除法函数
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);
}

运行结果:

1
结果: 5

优势: Result 类型让错误处理变得显式——调用方必须处理潜在的错误,而不能简单地忽略它。TypeScript 的类型系统会在编译期强制检查 ok 分支,访问 valueerror 时也能获得类型收窄。

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;
}

// 定义 Result 类型
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
// 定义 Result 类型
type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E };

// 通用错误处理包装函数
// 接受一个异步函数,返回 Result 类型
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 类型: 通过返回值处理错误,避免异常。
  • 错误封装: 创建通用错误处理函数,统一错误边界。

最佳实践: 根据具体场景选择合适的错误处理方式,在可读性与健壮性之间取得平衡。