面向 impatient programmers 的 JavaScript 书籍 (ES2022 版)
请支持本书:购买捐赠
(广告,请不要屏蔽。)

24 异常处理



本章介绍 JavaScript 如何处理异常。

  为什么 JavaScript 不更频繁地抛出异常?

JavaScript 直到 ES3 才支持异常。这解释了为什么该语言及其标准库很少使用它们。

24.1 动机:抛出和捕获异常

考虑以下代码。它将存储在文件中的配置文件读取到一个包含 Profile 类实例的数组中

function readProfiles(filePaths) {
  const profiles = [];
  for (const filePath of filePaths) {
    try {
      const profile = readOneProfile(filePath);
      profiles.push(profile);
    } catch (err) { // (A)
      console.log('Error in: '+filePath, err);
    }
  }
}
function readOneProfile(filePath) {
  const profile = new Profile();
  const file = openFile(filePath);
  // ··· (Read the data in `file` into `profile`)
  return profile;
}
function openFile(filePath) {
  if (!fs.existsSync(filePath)) {
    throw new Error('Could not find file '+filePath); // (B)
  }
  // ··· (Open the file whose path is `filePath`)
}

让我们检查一下在 B 行发生了什么:发生了一个错误,但处理该问题的最佳位置不是当前位置,而是 A 行。在那里,我们可以跳过当前文件并继续处理下一个文件。

因此

当我们抛出异常时,以下结构是活动的

readProfiles(···)
  for (const filePath of filePaths)
    try
      readOneProfile(···)
        openFile(···)
          if (!fs.existsSync(filePath))
            throw

throw 会逐个退出嵌套结构,直到遇到 try 语句。执行在该 try 语句的 catch 子句中继续。

24.2 throw

这是 throw 语句的语法

throw «value»;

24.2.1 我们应该抛出哪些值?

在 JavaScript 中可以抛出任何值。但是,最好使用 Error 的实例或子类,因为它们支持其他功能,例如堆栈跟踪和错误链接(请参阅 §24.4 “Error 及其子类”)。

这给我们留下了以下选项

24.3 try 语句

try 语句的最大版本如下所示

try {
  «try_statements»
} catch (error) {
  «catch_statements»
} finally {
  «finally_statements»
}

我们可以组合这些子句,如下所示

24.3.1 try 代码块

try 代码块可以被视为语句的主体。这是我们执行常规代码的地方。

24.3.2 catch 子句

如果异常到达 try 代码块,则将其分配给 catch 子句的参数,并执行该子句中的代码。接下来,执行通常在 try 语句之后继续。如果出现以下情况,则可能会发生变化

以下代码演示了在 A 行抛出的值确实在 B 行被捕获。

const errorObject = new Error();
function func() {
  throw errorObject; // (A)
}

try {
  func();
} catch (err) { // (B)
  assert.equal(err, errorObject);
}
24.3.2.1 省略 catch 绑定 [ES2019]

如果我们对抛出的值不感兴趣,我们可以省略 catch 参数

try {
  // ···
} catch {
  // ···
}

这有时可能很有用。例如,Node.js 具有 API 函数 assert.throws(func),用于检查 func 内部是否抛出错误。它可以实现如下。

function throws(func) {
  try {
    func();
  } catch {
    return; // everything OK
  }
  throw new Error('Function didn’t throw an exception!');
}

但是,此函数的更完整的实现将具有 catch 参数,并且例如会检查其类型是否符合预期。

24.3.3 finally 子句

finally 子句中的代码总是在 try 语句结束时执行,无论 try 代码块或 catch 子句中发生什么。

让我们看一个 finally 的常见用例:我们创建了一个资源,并且希望在使用完它后始终销毁它,无论在使用它时发生什么。我们将按如下方式实现

const resource = createResource();
try {
  // Work with `resource`. Errors may be thrown.
} finally {
  resource.destroy();
}
24.3.3.1 finally 始终执行

finally 子句始终执行,即使抛出错误(A 行)

let finallyWasExecuted = false;
assert.throws(
  () => {
    try {
      throw new Error(); // (A)
    } finally {
      finallyWasExecuted = true;
    }
  },
  Error
);
assert.equal(finallyWasExecuted, true);

即使有 return 语句(A 行)

let finallyWasExecuted = false;
function func() {
  try {
    return; // (A)
  } finally {
    finallyWasExecuted = true;
  }
}
func();
assert.equal(finallyWasExecuted, true);

24.4 Error 及其子类

Error 是所有内置错误类的公共超类。

24.4.1 类 Error

这就是 Error 的实例属性和构造函数的样子

class Error {
  // Instance properties
  message: string;
  cause?: any; // ES2022
  stack: string; // non-standard but widely supported

  constructor(
    message: string = '',
    options?: ErrorOptions // ES2022
  );
}
interface ErrorOptions {
  cause?: any; // ES2022
}

构造函数有两个参数

接下来的小节之后的子节将更详细地解释实例属性 .message.cause.stack

24.4.1.1 Error.prototype.name

每个内置错误类 E 都有一个属性 E.prototype.name

> Error.prototype.name
'Error'
> RangeError.prototype.name
'RangeError'

因此,有两种方法可以获取内置错误对象的类名

> new RangeError().name
'RangeError'
> new RangeError().constructor.name
'RangeError'
24.4.1.2 错误实例属性 .message

.message 仅包含错误消息

const err = new Error('Hello!');
assert.equal(String(err), 'Error: Hello!');
assert.equal(err.message, 'Hello!');

如果我们省略消息,则使用空字符串作为默认值(继承自 Error.prototype.message

如果我们省略消息,则它是空字符串

assert.equal(new Error().message, '');
24.4.1.3 错误实例属性 .stack

实例属性 .stack 不是 ECMAScript 功能,但它得到 JavaScript 引擎的广泛支持。它通常是一个字符串,但其确切结构没有标准化,并且在不同的引擎之间有所不同。

这就是它在 JavaScript 引擎 V8 上的样子

const err = new Error('Hello!');
assert.equal(
err.stack,
`
Error: Hello!
    at file://ch_exception-handling.mjs:1:13
`.trim());
24.4.1.4 错误实例属性 .cause [ES2022]

实例属性 .cause 是通过 new Error() 的第二个参数中的 options 对象创建的。它指定了哪个其他错误导致了当前错误。

const err = new Error('msg', {cause: 'the cause'});
assert.equal(err.cause, 'the cause');

有关如何使用此属性的信息,请参阅 §24.5 “链接错误”

24.4.2 Error 的内置子类

Error 具有以下子类 - 引用 ECMAScript 规范

24.4.3 子类化 Error

自 ECMAScript 2022 起,Error 构造函数接受两个参数(请参阅上一小节)。因此,我们在对其进行子类化时有两个选择:我们可以在子类中省略构造函数,也可以像这样调用 super()

class MyCustomError extends Error {
  constructor(message, options) {
    super(message, options);
    // ···
  }
}

24.5 链接错误

24.5.1 为什么我们要链接错误?

有时,我们会捕获在更深层嵌套的函数调用期间抛出的错误,并希望向其附加更多信息

function readFiles(filePaths) {
  return filePaths.map(
    (filePath) => {
      try {
        const text = readText(filePath);
        const json = JSON.parse(text);
        return processJson(json);
      } catch (error) {
        // (A)
      }
    });
}

try 子句中的语句可能会抛出各种错误。在大多数情况下,错误不会意识到导致它的文件的路径。这就是为什么我们想在 A 行附加该信息。

24.5.2 通过 error.cause 链接错误 [ES2022]

自 ECMAScript 2022 起,new Error() 允许我们指定导致它的原因

function readFiles(filePaths) {
  return filePaths.map(
    (filePath) => {
      try {
        // ···
      } catch (error) {
        throw new Error(
          `While processing ${filePath}`,
          {cause: error}
        );
      }
    });
}

24.5.3 .cause 的替代方案:自定义错误类

以下自定义错误类支持链接。它与 .cause 向前兼容。

/**
 * An error class that supports error chaining.
 * If there is built-in support for .cause, it uses it.
 * Otherwise, it creates this property itself.
 *
 * @see https://github.com/tc39/proposal-error-cause
 */
class CausedError extends Error {
  constructor(message, options) {
    super(message, options);
    if (
      (isObject(options) && 'cause' in options)
      && !('cause' in this)
    ) {
      // .cause was specified but the superconstructor
      // did not create an instance property.
      const cause = options.cause;
      this.cause = cause;
      if ('stack' in cause) {
        this.stack = this.stack + '\nCAUSE: ' + cause.stack;
      }
    }
  }
}

function isObject(value) {
  return value !== null && typeof value === 'object';
}

  练习:异常处理

exercises/exception-handling/call_function_test.mjs

  测验

请参阅 测验应用程序