告别 `any`:TypeScript 中 `try...catch` 的最佳实践
在 TypeScript 项目中,你是否经常为了通过编译而写出这种代码?
try {
// 某些逻辑
} catch (err: any) { // ❌ 违背了 TS 类型安全的初衷
console.log(err.message);
}
随着 TS 配置趋于严格,catch(err: any) 往往会触发 ESLint 警告或编译错误。本文将介绍处理 catch 块中错误对象的几种最佳实践。
1. 理解 unknown 的必然性
在现代 TypeScript(4.0+)中,推荐将捕获到的错误声明为 unknown。这是因为在运行时刻,你无法保证捕获到的一定是 Error 实例。
try {
throw "意外的错误字符串"; // 这里的错误甚至不是一个对象
} catch (err: unknown) {
// ❌ 报错:'err' is of type 'unknown'
// console.log(err.message);
}
2. 方案一:类型守卫(Type Guards)—— 最稳健的方法
这是官方推荐的做法。通过显式的 instanceof 检查,TS 会在代码块内自动收窄(Narrowing)类型。
try {
await fetchData();
} catch (err: unknown) {
if (err instanceof Error) {
// ✅ TS 现在知道 err 是 Error 类型
console.error(err.message);
console.error(err.stack);
} else {
// 处理非标准错误(如 throw "string")
console.error("发生了未知类型的错误", err);
}
}
3. 方案二:自定义工具函数(封装大法)
如果你觉得到处写 if (err instanceof Error) 太麻烦,可以封装一个工具函数。这是目前大型项目中最流行的做法。
编写工具函数
function toError(err: unknown): Error {
if (err instanceof Error) return err;
return new Error(String(err));
}
业务中使用
try {
doSomething();
} catch (err: unknown) {
const error = toError(err);
console.log(error.message); // ✅ 永远安全
}
4. 方案三:函数式处理(类似 Rust/Go)
如果你讨厌深层嵌套的 try...catch,可以使用封装好的包装器,将错误作为返回值返回。
async function safeRun<T>(promise: Promise<T>): Promise<[Error | null, T | null]> {
try {
const data = await promise;
return [null, data];
} catch (err: unknown) {
return [toError(err), null];
}
}
// 使用:
const [err, data] = await safeRun(fetchUser(id));
if (err) {
handle(err);
} else {
render(data);
}
5. 进阶:处理 Axios 等库的特定错误
如果你在使用 Axios,可以使用它内置的类型守卫:
import axios from 'axios';
try {
await axios.get('/api/user');
} catch (err: unknown) {
if (axios.isAxiosError(err)) {
// 这里可以访问 err.response, err.status 等特有属性
console.log(err.response?.data);
}
}
总结:该选哪一个?
| 场景 | 推荐做法 |
|---|---|
| 临时处理/小型脚本 | if (err instanceof Error) |
| 标准大型项目 | 封装 toError() 工具函数,确保类型安全 |
| 追求代码扁平化 | 采用 safeRun 包装器返回 [err, data]
|
| 第三方库请求 | 优先使用库提供的 isError 判断函数 |
核心原则: 永远不要相信 catch 捕获到的内容,永远在访问属性前进行类型检查。这不仅是过编译的要求,更是写出健壮代码的基石。