理解 try-catch 结构:错误处理的艺术

891 阅读7分钟

前言

在编程中,错误是不可避免的,无论你的代码有多严谨,总会遇到无法预料的问题。为了优雅的处理这些问题,JavaScript 引入了异常处理机制,其中最常见的就是 try-catch 结构。本文将探讨 try-catch 的工作原理、使用场景和最佳实践,希望能给读者带来帮助。

什么是 try-catch

通常,如果发生错误,脚本就会立即停止,并在控制台打印出来。但是有一种语法结构 try-catch,它使我们可以捕获错误,因此脚本可以执行更合理的操作,而不是“死掉”。

try-catch 结构由两部分组成,trycatch

try {
  // 代码
} catch (error) {
  // 错误捕获
}

它按照以下步骤执行:

  1. 首先执行 try {...} 中的代码。

  2. 如果没有错误,则忽略 catch (error):执行到 try 的末尾并跳过 catch 继续执行。

  3. 如果出现错误,则 try 执行停止,控制流转向 catch (error) 。变量 error 将包含一个 error 对象,该对象包含了所发生事件的详细信息。

所以,try {...} 块内的 error 不会杀死脚本,我们有机会在 catch 中处理它。

让我们来看一些例子:

  • 没有 error 的例子
try {
  alert('开始执行 try 中的内容');
  // ... 这里的代码没有错误
  alert('try 中的内容执行完毕');
} catch {err} {
  alert('catch 被忽略,因为没有 error');
}

  • 有 error 的例子
try {
  alert('开始执行 try 中的内容');
  // ... 这里的代码有错误
  alert('try 的末尾(未执行到此处)');
} catch {err} {
  alert('捕获到 error');
}

可选的 catch 绑定

如果我们不需要 error 的详细信息,catch 也可以忽略它:

try {
  // ...
} catch { // <-- 没有(err)
  // ...
}

try-catch 的局限性

try-catch 仅对运行时的 error 有效

要使得 try-catch 能工作,代码必须是可执行的。换句话说,它必须是有效的 JavaScript 代码。如果代码包含语法错误,try-catch 将无法正常工作,例如含有不匹配的花括号:

try {
  {{{{{{{
} catch (err) {
  alert('引擎无法理解这段代码,它是无效的'); // <-- 不会执行
}

JavaScript 引擎首先会读取代码,然后运行它。在读取阶段发生的错误被称为“解析时的错误”,并且无法恢复。这是因为引擎无法理解该代码。

所以,try-catch 只能处理有效代码中出现的错误。这类错误被称为“运行时的错误”,也被称为“异常”。

try-catch 无法捕获异步错误

如果在异步代码中发生异常,例如在 setTimeout 中,则 try-catch 不会捕获到异常:

try {
  setTimeout(() => {
    // ... 错误代码
  }, 1000);
} catch (err) {
  alert('不工作'); // <-- 不执行
}

因为 try-catch 包裹了异步函数,该函数要稍后执行,这时引擎已经离开了 try-catch 结构。

为了捕获到异步函数中的异常,那么 try-catch 必须在这个函数内部:

setTimeout(() => {
  try {
    // ... 错误代码
  } catch (err) {
    alert('error 在这里被捕获了');
  }
}, 1000);

使用 try-catch

现在来探究一下真实场景中 try-catch 的用例。

我们都知道,JavaScript 支持 JSON.parse(str) 方法来解析 JSON 编码的值。通常,它被用来解析从网络、服务器或是其他来源收到的数据。我们收到数据后,像下面这样调用 JOSN.parse

let json = '{"name": "Jack", "age": 18}';
let user = JSON.parse(json); // 将文本转换成 JavaScript 对象

alert(user.name); // Jack
alert(user.age); // 18

如果 json 格式错误,JOSN.parse 就会生成一个 error,因此脚本就会“死亡”。

让我们来用 try-catch 处理这个 error:

let json = '{ bad json }';

try {
  let user = JSON.parse(json); // <-- 当出现 error 时
  alert(user.name); // 不工作
} catch (err) {
  // ... 执行会跳到这里并继续执行
  alert('数据有错误');
}

在这里,我们在 catch 块中仅仅显示了错误信息,但我们可以做更多的事情,比如发送一个新的请求,向用户建议一个替代方案等等。所有这些,都比代码“死掉”好得多。

throw 操作符

如果这个 json 在语法上是正确的,但是没有必须的 name 属性该怎么办?像这样:

let json = '{ "age": 18 }';

try {
  let user = JSON.parse(json); // <-- 没有 error
  alert(user.name); // <-- 没有 name
} catch (error) {
  alert('不工作'); // <-- 不执行
}

这里 JSON.parse 正常执行,但缺少 name 属性对我们来说确实是个 error。为了统一进行 error 处理,我们将使用 throw 操作符。

throw 操作符将会生成一个 error 对象,语法如下:

throw <error object>

在上面的例子中,缺少 name 属性就是一个 error,因为用户必须有一个 name。所以,让我们抛出这个 error:

let json = '{ "age": 18 }'; // <-- 不完整的数据

try {
  let user = JSON.parse(json); // <-- 没有 error
  
  if (!user.name) {
    throw new SyntaxErro('数据不全,没有 name');
  }
  
  alert(user.name);
  
} catch (error) {
  alert('JSON Error: ' + error.message); // JSON Error: 数据不全,没有 name
}

throw 操作符生成了包含我们给定的 messageSyntaxErro,与 JavaScript 自己生成的方式相同。try 的执行立即停止,控制流转向 catch 块。

现在,catch 成为了所有 error 处理的唯一场所:对于 JSON.parse 和其他情况都适用。

try-catch-finally

try-catch 结构可能还有一个代码子句:finally

如果它存在,它在所有情况下都会被执行:

  • try 之后,如果没有 error。
  • catch 之后,如果有 error。

该扩展语法如下所示:

try {
  // ... 尝试执行的代码
} catch (error) {
  // ... 处理 error
} finally {
  // ... 总是会执行的代码
}

finally 子句通常用在:当我们开始做某事的时候,希望无论出现什么情况都要完成某个任务。

函数最终以 return 还是 throw 完成都无关紧要,在这两种情况下都会执行 finally 子句:

function f() {
  try {
    let bool = true;
    if (bool) return;
  } catch (error) {
    // ... 没有错误,不会执行
  } finally {
    // ... 始终执行
  }
}

f();
function f() {
  try {
    throw new Error(); // <-- 抛出一个错误
  } catch (error) {
    // 捕获错误,执行错误处理代码
  } finally {
    // ... 始终执行
  }
}

f();

try-finally

没有 catch 子句的 try-finally 结构也很有用。当我们不想在原地处理 error,但是需要确保我们启动的处理需要被完成时,我们应当使用它:

function f() {
  try {
    // ...
  } finally {
   // ... 完成必须要完成的事,即使 try 中的执行失败了
  }
}

上面的代码中,由于没有 catch,所以 try 中的 error 总是会使代码执行跳转至函数外部。但是,在跳出之前必须要执行 finally 中的代码。

何时使用 try-catch ?

尽管 try-catch 是处理异常的强大工具,但并不意味着你应该在每一行代码周围使用它。以下是一些使用 try-catch 的最佳实践:

  • 捕获特定的异常:尽量捕获具体的异常类型,以便针对性地处理问题,而不是简单得捕获所有异常。

  • 避免滥用: 不要使用 try-catch 来控制程序的逻辑流程。它应该仅用于处理不可预见的错误。

  • 记录异常:在 catch 块中,记录异常信息是很重要的,这有助于后续的调试和维护。

  • 清理工作:利用 finally 块来处理清理工作,确保资源被妥善释放。

  • 用户友好的错误处理:在捕获异常后,考虑给用户一个友好的提示,而不是简单的输出错误堆栈。

总结

try-catch 机制是编程中的一项重要技术,用于提高程序的健壮性和可靠性。通过合理的使用异常处理结构,我们可以有效地捕获和处理错误,从而提升用户体验。

在设计和实现异常处理策略时,牢记最佳实践,将使你的代码更易于维护,并在处理问题时提供更好的可调试性。

通过对 try-catch 的深入理解,你将能够更自信地编写高质量的代码,减少因未捕获异常而导致的程序崩溃。

如果文中有错误或者不足之处,欢迎大家在评论区指正

你的点赞,是对我最大的鼓励! 感谢阅读~