如果你有使用过像 Java、C++、或者 C# 这类编程语言的经验,你通常会透过抛出异常来做错误处理,然后使用一连串的 catch
语句来捕获它们。虽然已经证明有其他更好的方式来处理错误,但因为抛异常这种方式悠久的历史以及对编程习惯的广泛影响,我们依旧会在 JavaScript 当中继续使用它。
当然,这种方式的错误处理在 JavaScript 和 TypeScript 中是行之有效的,但如果你像下面这个例子一样照搬在其他编程语言的使用习惯,在 catch
语句里定义错误类型的话。
try {
// something with Axios, for example
} catch(e: AxiosError) {
// ^^^^^^^^^^ Error 1196 💥
}
你将收获一个 TS1196
错误:
Catch clause variable type annotation must be ‘any’ or ‘unknown’ if specified.
catch
语句变量类型只能声明为any
或者unknown
.
之所以这样,有下面几条理由:
1 . 你可以抛出任何类型
在 JavaScript 中, 你可以抛出任何的表达式。通常来说,我们会抛出 “异常” ( 在 JavaScript 中我们叫做 错误 Error ),但也不能排除我们会抛出其他任何类型的可能性:
throw "What a weird error"; // 👍
throw 404; // 👍
throw new Error("What a weird error"); // 👍
正因为任何合法的类型都能够被抛出,那 catch
语句能捕获到的类型也就不仅限于我们过去理解的错误或其子类型。
2 . JavaScript 中只能使用一个 catch
语句。尽管在很早之前就有了关于多路捕获甚至是在 catch
语句中加入条件表达式的提案,但直到今天这些你还不能使用这些新特性。
作为替代方案,我们可以通过在一个 catch
语句内部使用 instanceof
或者 typeof
类型检查来实现。
try {
myroutine(); // There's a couple of errors thrown here
} catch (e) {
if (e instanceof TypeError) {
// A TypeError
} else if (e instanceof RangeError) {
// Handle the RangeError
} else if (e instanceof EvalError) {
// you guessed it: EvalError
} else if (typeof e === "string") {
// The error is a string
} else if (axios.isAxiosError(e)) {
// axios does an error check for us!
} else {
// everything else
logMyErrors(e);
}
}
注:上面代码已经是在 typescript 中进行 收敛异常捕获 的最佳实践。
正是由于能够抛出任何可能的值,同时我们对一个 try
语句只能使用有且只有一个 catch
,那么这个 e
的类型范围如此的宽泛,就一点都不奇怪了。
3 . 无法预测的事情总在发生
那么你可能又要问了,在上面的例子里面,既然你知道所有可能发生的异常,我们就不能定义一个恰到好处的联合类型来表达它吗?
理论上是可以的,不过在实践中我们发现,根本无法穷举所有的异常类型。
除了一些用户自己定义的错误和异常,变量的类型不匹配,或者你的一个函数未定义,都会让系统抛出一个内存错误。更不用说,一个简单的方法超出了调用栈的最大数量,都会引起臭名昭著的 stack overflow。
异常的类型范围如此之宽广,而 catch
语句又只有一个,还要应对各种不确定的意外,就造成了这个 e
只能是 any
或 unknown
的局面。
Promise 的拒绝行为有什么不一样呢?
正确答案是:一毛一样。
typescript 只允许你去定义完成态(fullfilled)的值类型,对于拒绝行为(rejection),要么是你主动抛出了错误,或者系统发生了一场。
const somePromise = () => new Promise((fulfil, reject) => {
if (someConditionIsValid()) {
fulfil(42);
} else {
reject("Oh no!");
}
});
somePromise()
.then(val => console.log(val)) // val is number
.catch(e => {
console.log(e) // e can be anything, really.
})
如果你使用 asnyc/await
模式的话,它的表现会更符合上面的表达:
try {
const z = await somePromise(); // z is number
} catch(e) {
// same thing, e can be anything!
}
写在最后
很显然,对于从其他编程语言转移过来的开发者来讲,JavaScript 和 typescript 的错误处理并不十分友好。
我们能够做的,就是充分了解此间的差异,并坚信在不久的将来, typescript 团队给我们的类型检查能力,会让我们的错误处理足够的好。