2022 年 10 月 14 日

错误处理,“try...catch”

无论我们的编程能力有多强,有时我们的脚本都会有错误。它们可能是由于我们的错误、意外的用户输入、错误的服务器响应以及其他一千个原因造成的。

通常,脚本在发生错误时会“死亡”(立即停止),并将其打印到控制台。

但有一个语法结构 try...catch 允许我们“捕获”错误,因此脚本可以执行一些更合理的操作,而不是终止。

“try…catch”语法

try...catch 结构有两个主要块:try,然后是 catch

try {

  // code...

} catch (err) {

  // error handling

}

它的工作原理如下

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

因此,try {...} 块中的错误不会终止脚本 - 我们有机会在 catch 中处理它。

让我们看一些示例。

  • 无错误示例:显示 alert (1)(2)

    try {
    
      alert('Start of try runs');  // (1) <--
    
      // ...no errors here
    
      alert('End of try runs');   // (2) <--
    
    } catch (err) {
    
      alert('Catch is ignored, because there are no errors'); // (3)
    
    }
  • 带有错误的示例:显示 (1)(3)

    try {
    
      alert('Start of try runs');  // (1) <--
    
      lalala; // error, variable is not defined!
    
      alert('End of try (never reached)');  // (2)
    
    } catch (err) {
    
      alert(`Error has occurred!`); // (3) <--
    
    }
try...catch 仅适用于运行时错误

要使 try...catch 正常工作,代码必须可运行。换句话说,它应该是有效的 JavaScript。

如果代码语法错误,例如它有不成对的花括号,则它将不起作用

try {
  {{{{{{{{{{{{
} catch (err) {
  alert("The engine can't understand this code, it's invalid");
}

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

因此,try...catch 只能处理有效代码中发生的错误。此类错误称为“运行时错误”或有时称为“异常”。

try...catch 同步工作

如果异常发生在“已计划”的代码中,例如在 setTimeout 中,则 try...catch 不会捕获它

try {
  setTimeout(function() {
    noSuchVariable; // script will die here
  }, 1000);
} catch (err) {
  alert( "won't work" );
}

这是因为该函数本身稍后执行,此时引擎已经离开了 try...catch 结构。

要在计划函数中捕获异常,try...catch 必须在该函数内部

setTimeout(function() {
  try {
    noSuchVariable; // try...catch handles the error!
  } catch {
    alert( "error is caught here!" );
  }
}, 1000);

错误对象

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

try {
  // ...
} catch (err) { // <-- the "error object", could use another word instead of err
  // ...
}

对于所有内置错误,错误对象有两个主要属性

name
错误名称。例如,对于未定义的变量,它是 "ReferenceError"
message
有关错误详细信息的文本消息。

大多数环境中还有其他非标准属性。其中使用最广泛、支持最好的一个属性是

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

例如

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)

  // Can also show an error as a whole
  // The error is converted to string as "name: message"
  alert(err); // ReferenceError: lalala is not defined
}

可选的“catch”绑定

最近添加的功能
这是最近添加到该语言中的功能。旧浏览器可能需要 polyfill

如果我们不需要错误详细信息,catch 可以省略它

try {
  // ...
} catch { // <-- without (err)
  // ...
}

使用“try…catch”

让我们探索一下 try...catch 的实际用例。

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

通常,它用于解码从服务器或其他来源通过网络接收的数据。

我们接收它并像这样调用 JSON.parse

let json = '{"name":"John", "age": 30}'; // data from the server

let user = JSON.parse(json); // convert the text representation to JS object

// now user is an object with properties from the string
alert( user.name ); // John
alert( user.age );  // 30

您可以在 JSON 方法,toJSON 章节中找到有关 JSON 的更详细信息。

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

我们应该对此感到满意吗?当然不!

这样,如果数据出现问题,访问者将永远不知道(除非他们打开开发者控制台)。当某些东西“突然死掉”而没有任何错误消息时,人们真的不喜欢。

让我们使用 try...catch 来处理错误

let json = "{ bad json }";

try {

  let user = JSON.parse(json); // <-- when an error occurs...
  alert( user.name ); // doesn't work

} catch (err) {
  // ...the execution jumps here
  alert( "Our apologies, the data has errors, we'll try to request it one more time." );
  alert( err.name );
  alert( err.message );
}

这里我们只使用 catch 块来显示消息,但我们可以做更多事情:发送新的网络请求、向访问者建议替代方案、将有关错误的信息发送到日志记录工具,……所有这些都比直接死掉要好得多。

抛出我们自己的错误

如果 json 语法正确,但没有必需的 name 属性,该怎么办?

像这样

let json = '{ "age": 30 }'; // incomplete data

try {

  let user = JSON.parse(json); // <-- no errors
  alert( user.name ); // no name!

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

这里 JSON.parse 正常运行,但 name 的缺失实际上对我们来说是一个错误。

为了统一错误处理,我们将使用 throw 运算符。

“Throw”运算符

throw 运算符会生成一个错误。

语法是

throw <error object>

从技术上讲,我们可以将任何东西用作错误对象。它甚至可以是一个基元,例如数字或字符串,但最好使用对象,最好是具有 namemessage 属性的对象(以便与内置错误保持一定程度的兼容性)。

JavaScript 有许多用于标准错误的内置构造函数:ErrorSyntaxErrorReferenceErrorTypeError 等。我们也可以使用它们来创建错误对象。

它们的语法是

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

对于内置错误(不适用于任何对象,仅适用于错误),name 属性恰好是构造函数的名称。message 取自参数。

例如

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

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

让我们看看 JSON.parse 生成了哪种类型的错误

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 的缺失是一个错误,因为用户必须有 name

所以让我们抛出它

let json = '{ "age": 30 }'; // incomplete data

try {

  let user = JSON.parse(json); // <-- no errors

  if (!user.name) {
    throw new SyntaxError("Incomplete data: no name"); // (*)
  }

  alert( user.name );

} catch (err) {
  alert( "JSON Error: " + err.message ); // JSON Error: Incomplete data: no name
}

在行 (*) 中,throw 运算符使用给定的 message 生成一个 SyntaxError,就像 JavaScript 本身会生成它一样。try 的执行会立即停止,并且控制流会跳转到 catch

现在 catch 成为所有错误处理的单一位置:对于 JSON.parse 和其他情况。

重新抛出

在上面的示例中,我们使用 try...catch 来处理不正确的数据。但是,在 try {...} 块中是否可能发生另一个意外错误?就像编程错误(变量未定义)或其他一些事情,而不仅仅是这个“不正确的数据”问题。

例如

let json = '{ "age": 30 }'; // incomplete data

try {
  user = JSON.parse(json); // <-- forgot to put "let" before user

  // ...
} catch (err) {
  alert("JSON Error: " + err); // JSON Error: ReferenceError: user is not defined
  // (no JSON Error actually)
}

当然,一切皆有可能!程序员确实会犯错。即使在数百万用户使用了几十年的开源实用程序中——突然可能会发现一个导致可怕的攻击的错误。

在我们的案例中,try...catch 被放置为捕获“不正确的数据”错误。但从本质上讲,catch 会从 try 中获取所有错误。在这里,它会得到一个意外的错误,但仍然显示相同的 "JSON Error" 消息。这是错误的,并且也使代码更难调试。

为了避免此类问题,我们可以采用“重新抛出”技术。规则很简单

Catch 应该只处理它已知的错误,并“重新抛出”所有其他错误。

“重新抛出”技术可以更详细地解释为

  1. Catch 会获取所有错误。
  2. catch (err) {...} 块中,我们分析错误对象 err
  3. 如果我们不知道如何处理它,我们执行 throw err

通常,我们可以使用 instanceof 运算符检查错误类型

try {
  user = { /*...*/ };
} catch (err) {
  if (err instanceof ReferenceError) {
    alert('ReferenceError'); // "ReferenceError" for accessing an undefined variable
  }
}

我们还可以从 err.name 属性获取错误类名。所有本机错误都有它。另一个选项是读取 err.constructor.name

在下面的代码中,我们使用重新抛出,以便 catch 只处理 SyntaxError

let json = '{ "age": 30 }'; // incomplete data
try {

  let user = JSON.parse(json);

  if (!user.name) {
    throw new SyntaxError("Incomplete data: no name");
  }

  blabla(); // unexpected error

  alert( user.name );

} catch (err) {

  if (err instanceof SyntaxError) {
    alert( "JSON Error: " + err.message );
  } else {
    throw err; // rethrow (*)
  }

}

catch 块内部的 (*) 行上抛出的错误“跳出”try...catch,并且可以被外部 try...catch 结构(如果存在)捕获,或者它会终止脚本。

因此,catch 块实际上只处理它知道如何处理的错误,而“跳过”所有其他错误。

下面的示例演示了如何通过更多一级的 try...catch 捕获此类错误

function readData() {
  let json = '{ "age": 30 }';

  try {
    // ...
    blabla(); // error!
  } catch (err) {
    // ...
    if (!(err instanceof SyntaxError)) {
      throw err; // rethrow (don't know how to deal with it)
    }
  }
}

try {
  readData();
} catch (err) {
  alert( "External catch got: " + err ); // caught it!
}

此处 readData 只知道如何处理 SyntaxError,而外部 try...catch 知道如何处理所有内容。

try…catch…finally

等等,这还不是全部。

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

如果存在,它将在所有情况下运行

  • try 之后,如果没有错误,
  • catch 之后,如果有错误。

扩展语法如下所示

try {
   ... try to execute the code ...
} catch (err) {
   ... handle errors ...
} finally {
   ... execute always ...
}

尝试运行此代码

try {
  alert( 'try' );
  if (confirm('Make an error?')) BAD_CODE();
} catch (err) {
  alert( 'catch' );
} finally {
  alert( 'finally' );
}

代码有两种执行方式

  1. 如果你对“制造错误?”回答“是”,则为 try -> catch -> finally
  2. 如果你说“否”,则为 try -> finally

当我们开始做某事并希望在任何情况下都完成它时,通常会使用 finally 子句。

例如,我们希望测量斐波那契数函数 fib(n) 所花费的时间。自然地,我们可以在它运行之前开始测量,并在它运行之后结束测量。但是,如果在函数调用期间出现错误怎么办?特别是,下面代码中 fib(n) 的实现对负数或非整数返回一个错误。

无论如何,finally 子句都是完成测量的绝佳位置。

此处 finally 保证在两种情况下都能正确测量时间——在 fib 成功执行的情况下和在其中出现错误的情况下

let num = +prompt("Enter a positive integer number?", 35)

let diff, result;

function fib(n) {
  if (n < 0 || Math.trunc(n) != n) {
    throw new Error("Must not be negative, and also an integer.");
  }
  return n <= 1 ? n : fib(n - 1) + fib(n - 2);
}

let start = Date.now();

try {
  result = fib(num);
} catch (err) {
  result = 0;
} finally {
  diff = Date.now() - start;
}

alert(result || "error occurred");

alert( `execution took ${diff}ms` );

你可以通过在 prompt 中输入 35 来检查代码的运行情况——它正常执行,finallytry 之后。然后输入 -1——将立即出现错误,并且执行将花费 0ms。两种测量都正确完成。

换句话说,函数可能以 returnthrow 结束,这并不重要。finally 子句在这两种情况下都会执行。

变量在 try...catch...finally 中是局部的

请注意,上面代码中的 resultdiff 变量在 try...catch 之前声明。

否则,如果我们在 try 块中声明 let,它将只能在其中可见。

finallyreturn

finally 子句适用于从 try...catch任何退出。其中包括显式的 return

在下面的示例中,try 中有一个 return。在这种情况下,finally 在控制权返回到外部代码之前执行。

function func() {

  try {
    return 1;

  } catch (err) {
    /* ... */
  } finally {
    alert( 'finally' );
  }
}

alert( func() ); // first works alert from finally, and then this one
try...finally

没有 catch 子句的 try...finally 构造也很有用。当我们不想在此处处理错误(让它们贯穿)时,但希望确保我们启动的进程已完成时,我们应用它。

function func() {
  // start doing something that needs completion (like measurements)
  try {
    // ...
  } finally {
    // complete that thing even if all dies
  }
}

在上面的代码中,try 中的错误总是会抛出,因为没有 catch。但在执行流离开函数之前,finally 会起作用。

全局捕获

特定于环境

本部分的信息不属于核心 JavaScript 的一部分。

让我们想象一下,我们在 try...catch 之外遇到了致命错误,并且脚本已死机。就像编程错误或其他可怕的事情。

有没有办法对这样的情况做出反应?我们可能希望记录错误,向用户显示一些内容(通常他们看不到错误消息)等。

规范中没有,但环境通常会提供它,因为它非常有用。例如,Node.js 为此提供了 process.on("uncaughtException")。在浏览器中,我们可以将一个函数分配给特殊的 window.onerror 属性,该属性将在出现未捕获错误时运行。

语法

window.onerror = function(message, url, line, col, error) {
  // ...
};
message
错误消息。
url
发生错误的脚本的 URL。
linecol
发生错误的行号和列号。
error
错误对象。

例如

<script>
  window.onerror = function(message, url, line, col, error) {
    alert(`${message}\n At ${line}:${col} of ${url}`);
  };

  function readData() {
    badFunc(); // Whoops, something went wrong!
  }

  readData();
</script>

全局处理程序 window.onerror 的作用通常不是恢复脚本执行——在编程错误的情况下这可能是不可行的,而是将错误消息发送给开发人员。

还有一些 Web 服务为这种情况提供错误记录,例如 https://errorception.comhttps://www.muscula.com

它们的工作原理如下

  1. 我们在服务中注册并从他们那里获取一段 JS(或脚本 URL)以插入到页面中。
  2. 该 JS 脚本设置了一个自定义的 window.onerror 函数。
  3. 当发生错误时,它会向服务发送一个有关错误的网络请求。
  4. 我们可以登录到服务 Web 界面并查看错误。

总结

try...catch 结构允许处理运行时错误。它实际上允许“尝试”运行代码并“捕获”可能在其中发生的错误。

语法是

try {
  // run this code
} catch (err) {
  // if an error happened, then jump here
  // err is the error object
} finally {
  // do in any case after try/catch
}

可能没有 catch 部分或没有 finally,因此较短的结构 try...catchtry...finally 也是有效的。

错误对象具有以下属性

  • message - 人类可读的错误消息。
  • name - 带有错误名称(错误构造函数名称)的字符串。
  • stack(非标准,但得到很好的支持) - 错误创建时的堆栈。

如果不需要错误对象,我们可以使用 catch { 代替 catch (err) { 来省略它。

我们还可以使用 throw 运算符生成我们自己的错误。从技术上讲,throw 的参数可以是任何内容,但通常它是一个继承自内置 Error 类的错误对象。有关在下一章中扩展错误的更多信息。

重新抛出是错误处理的一个非常重要的模式:catch 块通常会预期并知道如何处理特定错误类型,因此它应该重新抛出它不知道的错误。

即使我们没有 try...catch,大多数环境也允许我们设置一个“全局”错误处理程序来捕获“掉出来”的错误。在浏览器中,那就是 window.onerror

任务

重要性:5

比较两个代码片段。

  1. 第一个使用 finallytry...catch 之后执行代码

    try {
      work work
    } catch (err) {
      handle errors
    } finally {
      cleanup the working space
    }
  2. 第二个片段将清理工作放在 try...catch 之后

    try {
      work work
    } catch (err) {
      handle errors
    }
    
    cleanup the working space

我们肯定需要在工作之后进行清理,无论是否有错误。

这里使用 finally 是否有优势,还是两个代码片段相等?如果存在这样的优势,那么请给出一个有意义的示例。

当我们查看函数内的代码时,差异变得显而易见。

如果“跳出”try...catch,则行为不同。

例如,当 try...catch 中有 return 时。无论通过 return 语句退出 try...catchfinally 子句都会起作用:try...catch 完成后立即执行,但在调用代码获得控制之前执行。

function f() {
  try {
    alert('start');
    return "result";
  } catch (err) {
    /// ...
  } finally {
    alert('cleanup!');
  }
}

f(); // cleanup!

…或者当有 throw 时,如下所示

function f() {
  try {
    alert('start');
    throw new Error("an error");
  } catch (err) {
    // ...
    if("can't handle the error") {
      throw err;
    }

  } finally {
    alert('cleanup!')
  }
}

f(); // cleanup!

在这里,finally 保证了清理。如果我们只是将代码放在 f 的末尾,它将不会在这些情况下运行。

教程地图

评论

在评论之前请阅读此内容…
  • 如果您有改进建议 - 请 提交 GitHub 问题 或提交拉取请求,而不是发表评论。
  • 如果您无法理解文章中的某些内容 - 请详细说明。
  • 要插入一些代码,请使用 <code> 标记,对于多行 - 将它们包装在 <pre> 标记中,对于 10 行以上 - 使用沙箱 (plnkrjsbincodepen…)