错误处理,"try...catch"

47 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第23天,点击查看活动详情

不管你多么精通编程,有时我们的脚本总还是会出现错误。可能是因为我们的编写出错,或是与预期不同的用户输入,或是错误的服务端响应以及其他数千种原因。

通常,如果发生错误,脚本就会“死亡”(立即停止),并在控制台将错误打印出来。

但是有一种语法结构 try...catch,它使我们可以“捕获(catch)”错误,因此脚本可以执行更合理的操作,而不是死掉。

“try…catch” 语法

try...catch 结构由两部分组成:try 和 catch

try {

  // 代码...

} catch (err) {

  // 错误捕获

}

它按照以下步骤执行:

  1. 首先,执行 try {...} 中的代码。
  2. 如果这里没有错误,则忽略 catch (err):执行到 try 的末尾并跳过 catch 继续执行。
  3. 如果这里出现错误,则 try 执行停止,控制流转向 catch (err) 的开头。变量 err(我们可以使用任何名称)将包含一个 error 对象,该对象包含了所发生事件的详细信息。

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

让我们来看一些例子。

  • 没有 error 的例子:显示 alert (1) 和 (2)

    try {
    
      alert('开始执行 try 中的内容');  // (1) <--
    
      // ...这里没有 error
    
      alert('try 中的内容执行完毕');   // (2) <--
    
    } catch (err) {
    
      alert('catch 被忽略,因为没有 error'); // (3)
    
    }
    
  • 包含 error 的例子:显示 (1) 和 (3) 行的 alert 中的内容:

    try {
    
      alert('开始执行 try 中的内容');  // (1) <--
    
    lalala; // error,变量未定义!
    
      alert('try 的末尾(未执行到此处)');  // (2)
    
    } catch (err) {
    
      alert(`出现了 error!`); // (3) <--
    
    }
    

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

要使得 try...catch 能工作,代码必须是可执行的。换句话说,它必须是有效的 JavaScript 代码。

如果代码包含语法错误,那么 try..catch 将无法正常工作,例如含有不匹配的花括号:

try {
  {{{{{{{{{{{{
} catch (err) {
  alert("引擎无法理解这段代码,它是无效的");
}

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

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

try...catch 同步执行

如果在“计划的(scheduled)”代码中发生异常,例如在 setTimeout 中,则 try...catch 不会捕获到异常:

try {
  setTimeout(function() {
    noSuchVariable; // 脚本将在这里停止运行
  }, 1000);
} catch (err) {
  alert( "不工作" );
}

因为 try...catch 包裹了计划要执行的函数,该函数本身要稍后才执行,这时引擎已经离开了 try...catch 结构。

为了捕获到计划的(scheduled)函数中的异常,那么 try...catch 必须在这个函数内:

setTimeout(function() {
  try {
    noSuchVariable; // try...catch 处理 error 了!
  } catch {
    alert( "error 被在这里捕获了!" );
  }
}, 1000);

Error 对象

发生错误时,JavaScript 会生成一个包含有关此 error 详细信息的对象。然后将该对象作为参数传递给 catch

try {
  // ...
} catch (err) { // <-- “error 对象”,也可以用其他参数名代替 err
  // ...
}

对于所有内建的 error,error 对象具有两个主要属性:

  • name

    Error 名称。例如,对于一个未定义的变量,名称是 "ReferenceError"

  • message

    关于 error 的详细文字描述。

还有其他非标准的属性在大多数环境中可用。其中被最广泛使用和支持的是:

  • stack

    当前的调用栈:用于调试目的的一个字符串,其中包含有关导致 error 的嵌套调用序列的信息。

例如:

try {
lalala; // error, variable is not defined!
} catch (err) {
  alert(err.name); // ReferenceError
  alert(err.message); // lalala is not defined
  alert(err.stack); // ReferenceError: lalala is not defined at (...call stack)

  // 也可以将一个 error 作为整体显示出来
  // error 信息被转换为像 "name: message" 这样的字符串
  alert(err); // ReferenceError: lalala is not defined
}

可选的 “catch” 绑定

最近新增的特性

这是一个最近添加到 JavaScript 的特性。 旧式浏览器可能需要 polyfills.

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

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

使用 “try…catch”

让我们一起探究一下真实场景中 try...catch 的用例。

正如我们所知道的,JavaScript 支持 JSON.parse(str) 方法来解析 JSON 编码的值。

通常,它被用来解析从网络、服务器或是其他来源接收到的数据。

我们收到数据后,然后像下面这样调用 JSON.parse

let json = '{"name":"John", "age": 30}'; // 来自服务器的数据

let user = JSON.parse(json); // 将文本表示转换成 JavaScript 对象

// 现在 user 是一个解析自 json 字符串的有自己属性的对象
alert( user.name ); // John
alert( user.age );  // 30

你可以在 JSON 方法,toJSON 一章中找到更多关于 JSON 的详细内容。

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

我们对此满意吗?当然不!

如果这样做,当拿到的数据出了问题,那么访问者永远都不会知道原因(除非他们打开开发者控制台)。代码执行失败却没有提示信息,这真的是很糟糕的用户体验。

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

let json = "{ bad json }";

try {

let user = JSON.parse(json); // <-- 当出现 error 时...
  alert( user.name ); // 不工作

} catch (err) {
// ...执行会跳转到这里并继续执行
  alert( "很抱歉,数据有错误,我们会尝试再请求一次。" );
  alert( err.name );
  alert( err.message );
}

在这儿,我们将 catch 块仅仅用于显示信息,但我们可以做更多的事:发送一个新的网络请求,向访问者建议一个替代方案,将有关错误的信息发送给记录日志的设备,……。所有这些都比代码“死掉”好得多。

抛出我们自定义的 error

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

像这样:

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

try {

  let user = JSON.parse(json); // <-- 没有 error
alert( user.name ); // 没有 name!

} catch (err) {
  alert( "doesn't execute" );
}

这里 JSON.parse 正常执行,但缺少 name 属性对我们来说确实是个 error。

为了统一进行 error 处理,我们将使用 throw 操作符。

“throw” 操作符

throw 操作符会生成一个 error 对象。

语法如下:

throw <error object>

技术上讲,我们可以将任何东西用作 error 对象。甚至可以是一个原始类型数据,例如数字或字符串,但最好使用对象,最好使用具有 name 和 message 属性的对象(某种程度上保持与内建 error 的兼容性)。

JavaScript 中有很多内建的标准 error 的构造器:ErrorSyntaxErrorReferenceErrorTypeError 等。我们也可以使用它们来创建 error 对象。

它们的语法是:

let error = new Error(message);
// 或
let error = new SyntaxError(message);
let error = new ReferenceError(message);
// ...

对于内建的 error(不是对于其他任何对象,仅仅是对于 error),name 属性刚好就是构造器的名字。message 则来自于参数(argument)。

例如:

let error = new Error("Things happen o_O");

alert(error.name); // Error
alert(error.message); // Things happen o_O

让我们来看看 JSON.parse 会生成什么样的 error:

try {
  JSON.parse("{ bad json o_O }");
} catch(err) {
alert(err.name); // SyntaxError
  alert(err.message); // Unexpected token b in JSON at position 2
}

正如我们所看到的, 那是一个 SyntaxError

在我们的示例中,缺少 name 属性就是一个 error,因为用户必须有一个 name

所以,让我们抛出这个 error。

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

try {

  let user = JSON.parse(json); // <-- 没有 error

  if (!user.name) {
throw new SyntaxError("数据不全:没有 name"); // (*)
  }

  alert( user.name );

} catch(err) {
  alert( "JSON Error: " + err.message ); // JSON Error: 数据不全:没有 name
}

在 (*) 标记的这一行,throw 操作符生成了包含着我们所给定的 message 的 SyntaxError,与 JavaScript 自己生成的方式相同。try 的执行立即停止,控制流转向 catch 块。

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