JavaScript 中的异常处理
尝试…捕捉
异步代码中的 try 块
由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!
立即订阅我的邮件列表:http: //jauyeung.net/subscribe/
请在Twitter上关注我:https://twitter.com/AuMayeung
更多文章请访问https://medium.com/@hohanga
和所有程序一样,JavaScript 也会遇到错误,例如 JSON 解析失败或变量中意外出现空值。这意味着,如果我们希望应用程序为用户提供良好的用户体验,就必须优雅地处理这些错误。错误通常以异常的形式出现,因此我们必须优雅地处理它们。为了处理try...catch这些错误,我们需要使用相应的语句来处理它们,以避免程序崩溃。
尝试…捕捉
要使用该try...catch代码块,我们需要使用以下语法:
try{
// code that we run that may raise exceptions
// one or more lines is required in this block
}
catch (error){
// handle error here
// optional if finally block is present
}
finally {
// optional code that run either
// when try or catch block is finished
}
例如,我们可以编写以下代码来捕获异常:
try {
undefined.prop
} catch (error) {
console.log(error);
}
在上面的代码中,我们试图从 `<object>` 中获取属性undefined,这显然是不允许的,因此抛出了异常。在代码块中catch,我们捕获了由运行引起的“TypeError: Cannot read property 'prop' of undefined”异常,并将异常undefined.prop输出记录下来。这样,我们就得到了输出的错误信息,而不是导致程序崩溃。
该try...catch语句包含一个try代码块。try代码块内必须至少包含一条语句,并且必须始终使用花括号,即使是单条语句也一样。然后,可以包含catch`if` 或finally`if` 子句。这意味着我们可以有:
try {
...
}
catch {
...
}
try {
...
}
finally{
...
}
try {
...
}
catch {
...
}
finally {
...
}
该catch子句包含用于指定当代码块中抛出异常时应执行的操作的代码try。如果try代码块执行失败并抛出异常,catch则会执行代码块中的代码。如果代码块中的所有代码都已执行完毕且未抛出任何异常,则会跳过代码块try中的代码。catch
该finally代码块会在代码try块本身或整个catch代码块执行完毕后执行。无论是否抛出异常,它都会执行。
try代码块可以相互嵌套。如果内部try代码块没有捕获到异常,而外部代码块包含catch异常捕获块,那么外部代码块将会捕获内部代码块抛出的异常try。例如,如果我们有:
try {
try {
undefined.prop
finally {
console.log('Inner finally block runs');
}
} catch (error) {
console.log('Outer catch block caught:', error);
}
如果我们运行上面的代码,应该会看到“内部 finally 代码块运行”和“外部 catch 代码块捕获到:TypeError:无法读取未定义对象的属性 'prop'”的日志,这符合预期,因为内部try代码块没有捕获到异常,catch所以由外部catch代码块捕获。我们可以看到,内部 finally 代码块在外部 catch 代码块之前执行。代码try...catch...finally是按顺序执行的,所以先添加的代码会在后添加的代码之前执行。
catch我们目前编写的代码块都是无条件的。这意味着它们会捕获所有抛出的异常。该对象error保存着关于抛出异常的数据。它只保存代码catch块内部的数据。如果我们想在代码块外部保留这些数据,则必须将其赋值给catch代码块外部的一个变量。catch代码块执行完毕后,该error对象将不再可用。
该finally子句包含在代码try块或catch代码块执行之后、但在代码块下方的语句执行之前会被忽略的语句try...catch...finally。无论是否抛出异常,该子句都会执行。如果抛出异常,即使没有代码块捕获并处理该异常,该子句中的语句finally也会执行catch。
因此,` finallyfailure` 代码块非常实用,可以让程序在发生错误时优雅地终止。例如,我们可以将一些无论是否抛出异常都会运行的清理代码(例如用于关闭文件读取句柄的代码)放在 `failure` 代码块中。try当 `failure` 代码块中的某行代码在执行过程中抛出异常时try,该代码块中的其余代码不会执行。因此,如果我们预期在 `failure` 代码块中关闭文件句柄try,但在执行关闭文件句柄的代码行之前抛出了异常,那么为了优雅地结束程序,我们应该将该操作放在 ` finallyfailure` 代码块中,以确保文件句柄始终被清理。我们可以将一些无论是否抛出异常都会运行的代码(例如清理代码)放在 `failure` 代码finally块中,这样就无需在 `failure`try和 `failure`catch代码块中重复编写这些代码。例如,我们可以这样写:
openFile();
try {
// tie up a resource
writeFile(data);
}
finally {
closeFile();
// always close the resource
}
上面的代码中,closeFile无论writeFile运行时是否抛出异常,该函数始终都会运行,从而消除了重复代码。
我们可以使用嵌套try代码块,如下面的代码所示:
try {
try {
throw new Error('error');
}
finally {
console.log('finally runs');
}
}
catch (ex) {
console.error('exception caught', ex.message);
}
如果我们查看控制台日志,应该会看到“finally runs”出现在“exception caught error”之前。这是因为代码try...catch块中的所有内容都是逐行执行的,即使是嵌套的代码块。如果我们有更多嵌套,例如以下代码:
try {
try {
throw new Error('error');
}
finally {
console.log('first finally runs');
}
try {
throw new Error('error2');
}
finally {
console.log('second finally runs');
}
}
catch (ex) {
console.error('exception caught', ex.message);
}
我们看到控制台日志输出与之前相同。这是因为第一个内部try代码块没有捕获到异常,所以异常被传递到外部catch代码块并被其捕获。如果我们想让第二个try代码块运行,则需要catch向第一个try代码块添加一个额外的代码块,如下例所示:
try {
try {
throw new Error('error');
}
catch {
console.log('first catch block runs');
}
finally {
console.log('first finally runs');
}
try {
throw new Error('error2');
}
finally {
console.log('second finally runs');
}
}
catch (ex) {
console.error('exception caught', ex.message);
}
现在我们看到按顺序记录了以下消息:“第一个 catch 块运行”、“第一个 finally 块运行”、“第二个 finally 块运行”、“捕获到异常 error2”。这是因为第一个trycatch 块本身包含一个catchcatch 块,所以该行代码引发的异常throw new Error('error')现在被catch第一个内部trycatch 块捕获。而第二个内部trycatch 块没有关联的catchcatch 块,因此error2异常将被外部 catch 块捕获catch。
我们还可以重新抛出代码块中捕获到的错误catch。例如,我们可以编写以下代码来实现这一点:
try {
try {
throw new Error('error');
}
catch (error) {
console.error('error', error.message);
throw error;
} finally {
console.log('finally block is run');
}
} catch (error) {
console.error('outer catch block caught', error.message);
}
如我们所见,如果运行上述代码,则会按顺序记录以下信息:“error error”、“finally block is run”和“outer catch block caught error”。这是因为内部代码catch块记录了 `finally` 语句抛出的异常throw new Error(‘error’),但在 `finally` 语句console.error(‘error’, error.message);执行后,我们又运行了 `finally` 语句throw error;再次抛出该异常。然后内部finally代码块执行,再次抛出的异常被外部代码块捕获,catch并记录了内部代码块中 `finally` 语句error再次抛出的异常。throw errorcatch
由于代码是顺序执行的,我们可以在代码块return末尾执行语句try。例如,如果我们想将一个 JSON 字符串解析成一个对象,并且希望在解析传入的字符串时出错(例如,传入的字符串不是有效的 JSON 字符串)时返回一个空对象,那么我们可以编写以下代码:
const parseJSON = (str) => {
try {
return JSON.parse(str);
}
catch {
return {};
}
}
上面的代码会JSON.parse解析字符串,如果它不是有效的 JSON,则会抛出异常。如果抛出异常,catch则会调用相应的子句返回一个空对象。如果JSON.parse解析成功,则会返回已解析的 JSON 对象。所以,如果我们运行:
console.log(parseJSON(undefined));
console.log(parseJSON('{"a": 1}'))
然后我们在第一行得到一个空对象,然后{a: 1}在第二行得到。
异步代码中的 try 块
使用 `return`async和 `return` await,我们可以简化 Promise 代码。在使用 `return` 之前async,await我们需要使用then`return` 函数,并将回调函数作为参数传递给所有then函数。如果 Promise 数量很多,这会导致代码冗长。现在,我们可以使用 `return`async和 ` awaitreturn` 语法来替换 `return`then及其关联的回调函数,如下所示。使用 `return`async和 ` awaitreturn` 语法来链式调用 Promise,我们还可以使用`return`try和 `return`catch代码块来捕获被拒绝的 Promise 并优雅地处理它们。例如,如果我们想使用 `return`catch代码块捕获 Promise 拒绝,我们可以这样做:
(async () => {
try {
await new Promise((resolve, reject) => {
reject('error')
})
}
catch (error) {
console.log(error);
}
})();
在上面的代码中,由于我们拒绝了在try代码块中定义的 Promise,catch代码块捕获了 Promise 的拒绝并记录了错误。因此,运行上面的代码时,我们应该会看到日志输出“error”。虽然它看起来像一个普通的try...catch代码块,但实际上并非如此,因为这是一个async函数。async函数只能返回 Promise,所以我们不能在try...catch代码块中返回 Promise 以外的任何值。函数catch中的代码块async只是该catch函数的简写形式,它链接到 then 函数。因此,上面的代码实际上等价于:
(() => {
new Promise((resolve, reject) => {
reject('error')
})
.catch(error => console.log(error))
})()
async我们看到,当运行上述函数时,会得到相同的控制台日志输出。
该代码块也可以在函数中finally使用。例如,我们可以这样写:try...catchasync
(async () => {
try {
await new Promise((resolve, reject) => {
reject('error')
})
} catch (error) {
console.log(error);
} finally {
console.log('finally is run');
}
})();
在上面的代码中,由于我们拒绝了在代码块中定义的 Promise try,该catch代码块捕获了 Promise 的拒绝并记录了错误。因此,运行上面的代码时,我们应该看到日志记录了“error”。代码块finally执行完毕后,我们会看到日志记录了“finally is run”。函数finally中的代码块async等同于将finally函数链接到 Promise 的末尾,因此上面的代码等效于:
(() => {
new Promise((resolve, reject) => {
reject('error')
})
.catch(error => console.log(error))
.finally(() => console.log('finally is run'))
})()
async我们看到,当运行上述函数时,会得到相同的控制台日志输出。
try...catch我们上面提到的嵌套规则仍然适用于async函数,所以我们可以这样写:
(async () => {
try {
await new Promise((resolve, reject) => {
reject('outer error')
})
try {
await new Promise((resolve, reject) => {
reject('inner error')
})
}
catch (error) {
console.log(error);
}
finally { }
}
catch (error) {
console.log(error);
}
finally {
console.log('finally is run');
}
})();
这样我们就可以轻松地嵌套 Promise 并相应地处理它们的错误。这比之前使用链式调用then`and`catch和`functions`函数的方式更简洁。finallyasync
为了处理 JavaScript 程序中的错误,我们可以使用 ` try...catch...finallyif` 代码块来捕获错误。这可以用于同步代码或异步代码。我们将可能抛出异常的代码放在 `if`try代码块中,然后将处理异常的代码放在 `if`catch代码块中。在 `if`finally代码块中,无论是否抛出异常,都会执行所有需要运行的代码。async函数也可以使用try...catch`if` 代码块,但它们像其他async函数一样只能返回 Promise,而try...catch...finally普通函数中的 `if` 代码块可以返回任何类型的值。