JavaScript错误处理完全指南

前端之巅 2020-09-17 15:22
作者丨VALENTINO GAGLIARDI
译者丨王强
策划丨小智
本文将介绍如何处理同步和异步 JavaScript 代码中的错误和异常。建议收藏!

本文最初发布于 valentinog.com 网站,经原作者授权由 InfoQ 中文站翻译并分享。

什么是编程中的错误?

在我们的程序中,事物并非总是一帆风顺的

特别是在某些情况下,我们可能希望 停止程序或在发生意外错误时通知用户

例如:

  • 程序试图打开一个不存在的文件。

  • 网络连接断开。

  • 用户输入了无效的内容。

在所有这些情况下,我们程序员都会创建 错误,或者让编程引擎为我们创建一些错误。

在创建错误之后,我们可以向用户发送一条消息,或者完全停止执行。

JavaScript 中有什么错误?

JavaScript 中的一个错误是一个对象,错误会被 抛出 以暂停程序。

要在 JavaScript 中创建一个新错误,我们需要调用适当的 构造函数。例如,要创建一个新的泛型错误,我们可以执行以下操作:
const err = new Error("Something bad happened!");
创建一个错误对象时,也可以省略 new 关键字:
const err = Error("Something bad happened!");

创建后,错误对象将显示三个属性:

  • message:包含错误消息的字符串。

  • name:错误的类型。

  • stack:函数执行的堆栈跟踪。

例如,如果我们创建一个新的 TypeError 对象,带有适当的消息,该 message 将携带实际的错误字符串,而 name 将为“TypeError”:
const wrongType = TypeError("Wrong type given, expected number");
wrongType.message; // "Wrong type given, expected number"
wrongType.name; // "TypeError"

Firefox 还实现了一些非标准属性,如 columnNumber、filename 和 lineNumber。

JavaScript 中的错误类型

JavaScript 中有很多错误类型,包括:

  • Error

  • EvalError

  • InternalError

  • RangeError

  • ReferenceError

  • SyntaxError

  • TypeError

  • URIError

请记住,所有这些错误类型都是 实际的构造函数,旨在返回一个新的错误对象。

在代码中,你将主要使用 Error 和 TypeError 这两种最常见的类型来创建自己的错误对象。

一般来说,大多数错误将直接来自 JavaScript 引擎,例如 InternalError 或 SyntaxError。

当你尝试重赋值 const 时,会发生 TypeError:
const name = "Jules";
name = "Caty";
// TypeError: Assignment to constant variable.
当你的语言关键字拼写错误时,会发生 SyntaxError:
va x = '33';
// SyntaxError: Unexpected identifier
或者,当你在错误的地方使用保留的关键字时,例如在一个 async 函数外部 await:
function wrong(){
    await 99;
}
wrong();
// SyntaxError: await is only valid in async function
当我们在页面中选择不存在的 HTML 元素时,也会发生 TypeError:
Uncaught TypeError: button is null

除了这些传统的错误对象外,JavaScript 中很快还会有 AggregateError 对象。AggregateError 可以很容易地将多个错误包装在一起,后文会具体介绍。

除了这些内置错误外,在浏览器中我们还可以找到:

  • DOMException。

  • DOMError,已弃用,如今不再使用。

DOMException 是与 WebAPI 相关的一系列错误。当我们在浏览器中做蠢事时它们就会被抛出,例如:
document.body.appendChild(document.cloneNode(true));
结果:
Uncaught DOMException: Node.appendChild: May not add a Document as a child

有关完整列表,请参见 MDN 上的这一页面:

https://developer.mozilla.org/en-US/docs/Web/API/DOMException

什么是异常?

多数开发人员认为错误和异常是同一回事。实际上,一个错误对象只有在被抛出时才成为异常

要在 JavaScript 中抛出一个异常,我们使用 throw,然后是错误对象:
const wrongType = TypeError("Wrong type given, expected number");
throw wrongType;
缩写形式更常见,在大多数代码库中你都可以找到:
throw TypeError("Wrong type given, expected number");
或:
throw new TypeError("Wrong type given, expected number");
不太可能将异常抛出到函数或条件块之外。相反,考虑以下示例:
function toUppercase(string) {
  if (typeof string !== "string") {
    throw TypeError("Wrong type given, expected a string");
  }
  return string.toUpperCase();
}
在这里,我们检查这个函数参数是否为一个字符串。如果不是,我们抛出一个异常。从技术上讲,你可以在 JavaScript 中抛出任何内容,而不仅仅是错误对象:
throw Symbol();
throw 33;
throw "Error!";
throw null;

但最好避免这些事情,始终抛出正确的错误对象,而不是基元。这样,你就可以在代码库中保持错误处理的一致性。其他团队成员就能一直在错误对象上访问 error.message 或 error.stack。

当我们抛出异常时会发生什么?

异常就像在上升的电梯:一旦抛出一个,它就会在程序栈中冒泡,除非它在某个地方被捕获

考虑以下代码:
function toUppercase(string) {
  if (typeof string !== "string") {
    throw TypeError("Wrong type given, expected a string");
  }
  return string.toUpperCase();
}
toUppercase(4);
如果你在浏览器或 Node.js 中运行此代码,程序将停止并报告错误:
Uncaught TypeError: Wrong type given, expected a string
    toUppercase http://localhost:5000/index.js:3
    <anonymous> http://localhost:5000/index.js:9

此外,你可以看到发生错误的具体代码行。这个报告是一个 堆栈跟踪,对于跟踪代码中的问题很有帮助。

堆栈跟踪的顺序是从底到顶的。所以在这里:
toUppercase http://localhost:5000/index.js:3
    <anonymous> http://localhost:5000/index.js:9

我们可以说:

  • 第 9 行中的代码调用了 toUppercase

  • toUppercase 在第 3 行爆炸了

除了在浏览器的控制台中看到这个堆栈跟踪外,你还可以在错误对象的 stack 属性上访问它。

如果这个异常 未捕获,即程序员没有采取任何措施来捕获它,则程序将崩溃。

在何时何地捕获代码中的异常取决于具体的用例

例如,你可能想在堆栈中传播一个异常,以使程序完全崩溃。出现致命的错误时可能就会是这种情况,因为停止程序比处理无效数据更安全。

介绍了基础知识之后,现在我们来研究 同步和异步 JavaScript 代码中的错误和异常处理

同步错误处理

同步代码在大多数情况下很简单,它的错误处理也是如此。

常规函数的错误处理
同步代码的执行顺序和代码的编写顺序一致。再来看前面的示例:
function toUppercase(string) {
  if (typeof string !== "string") {
    throw TypeError("Wrong type given, expected a string");
  }
  return string.toUpperCase();
}
toUppercase(4);
在这里,引擎调用并执行 toUppercase。所有这些都是 同步 发生的。要 捕获 由此类同步函数引发的异常,我们可以使用 try/catch/finally:
try {
  toUppercase(4);
} catch (error) {
  console.error(error.message);
  // or log remotely
} finally {
  // clean up
}

通常,try 处理最简单的场景,或可能抛出错误的函数调用。catch 则会 捕获实际的异常。它 接收错误对象,我们可以检查该错误对象(并将其远程发送到生产环境中的某些记录器)。

另一方面,无论函数的结果如何,finally 语句都会运行:无论是失败还是成功,final 内部的任何代码都将运行。

记住:try/catch/finally 是一个 同步 结构:它现在具有捕获来自异步代码异常的方法。

生成器函数的错误处理

JavaScript 中的生成器(generator)函数是一种特殊的函数。

除了在其内部作用域和消费者之间提供 双向通信通道 外,它可以 随意暂停和恢复

要创建一个生成器函数,我们在 function 关键字后加一个星号 *:
function* generate() {
//
}
一旦进入函数,我们就可以使用 yield 来返回值:
function* generate() {
  yield 33;
  yield 99;
}

生成器函数的返回值迭代器(iterator)对象。为了 从生成器中提取值,我们可以使用两种方法:

  • 在迭代器对象上调用 next()。

  • for...of 的 迭代

以我们的示例为例,要从生成器获取值,我们可以这样做:
function* generate() {
  yield 33;
  yield 99;
}
const go = generate();
当我们调用生成器函数时,go 成为我们的迭代器对象。从现在开始,我们可以调用 go.next() 来推进执行:
function* generate() {
  yield 33;
  yield 99;
}
const go = generate();
const firstStep = go.next().value; // 33
const secondStep = go.next().value; // 99

生成器也有另一种工作机制:它们可以接受调用者返回的值和异常。除了 next() 之外,从生成器返回的迭代器对象还具有 throw() 方法。

使用这种方法,我们可以将异常注入生成器来暂停程序:
function* generate() {
  yield 33;
  yield 99;
}
const go = generate();
const firstStep = go.next().value; // 33
go.throw(Error("Tired of iterating!"));
const secondStep = go.next().value; // never reached
要捕获此类错误,你可以使用 try/catch 将代码包装在生成器中(如果需要的话也可以用 finally):
function* generate() {
  try {
    yield 33;
    yield 99;
  } catch (error) {
    console.error(error.message);
  }
}

生成器函数还可以向外部抛出异常。捕获这些异常的机制与捕获同步异常的机制相同:try/catch/finally。

这是一个从外部使用 for...of 消费的生成器函数的示例:
function* generate() {
  yield 33;
  yield 99;
  throw Error("Tired of iterating!");
}
try {
  for (const value of generate()) {
    console.log(value);
  }
} catch (error) {
  console.error(error.message);
}
/* Output:
33
99
Tired of iterating!
*/

在这里,我们迭代 try 块中的 happy path。如果发生任何异常,我们将使用 catch 停止它。

异步错误处理

JavaScript 本质上是同步的,是一种单线程语言。

浏览器引擎之类的主机环境使用许多 WebAPI 增强了 JavaScript,以同外部系统交互并处理 I/O 相关联的操作。

浏览器中的异步性示例包括超时、事件和 Promise。

异步世界中的错误处理 与同步世界是不一样的。

我们来看一些例子。

计时器错误处理

开始探索 JavaScript 时,在学习了 try/catch/finally 之后,你可能会想将它放在任何代码块中。

考虑以下代码段:
function failAfterOneSecond() {
  setTimeout(() => {
    throw Error("Something went wrong!");
  }, 1000);
}
此函数将在大约 1 秒钟后抛出错误。处理此异常的正确方法是什么?以下示例 不起作用
function failAfterOneSecond() {
  setTimeout(() => {
    throw Error("Something went wrong!");
  }, 1000);
}
try {
  failAfterOneSecond();
} catch (error) {
  console.error(error.message);
}

正如我们所说,try/catch 是同步的。另一方面,我们有 setTimeout,这是一个用于计时器(timer)的浏览器 API。到传递给 setTimeout 的回调运行时,我们的 try/catch早就没了。该程序将崩溃,因为我们无法捕获异常。

它们走的是两条不同的路径:
Track A: --> try/catch
Track B: --> setTimeout --> callback --> throw

如果我们不想让程序崩溃,为了正确处理错误,我们必须在 setTimeout 的回调内移动 try/catch。但是,这种方法在大多数情况下没有多大意义。稍后我们将看到,使用 Promises 进行异步错误处理可提供更好的开发体验

事件错误处理

文档对象模型(DOM)中的 HTML 节点连接到 EventTarget,EventTarget 是浏览器中任何事件发射器(emitter)的公共祖先。

这意味着我们可以侦听页面中任何 HTML 元素上的事件:

https://www.valentinog.com/blog/event/#how-does-event-driven-applies-to-javascript-in-the-browser

(Node.js 会在未来版本中支持 EventTarget)。

DOM 事件的错误处理机制遵循异步 WebAPI 的模式

考虑以下示例:
const button = document.querySelector("button");
button.addEventListener("click", function() {
  throw Error("Can't touch this button!");
});
在这里,单击按钮后立即抛出一个异常。我们如何捕获它呢?这个模式 不起作用,也不会阻止程序崩溃:
const button = document.querySelector("button");
try {
  button.addEventListener("click", function() {
    throw Error("Can't touch this button!");
  });
} catch (error) {
  console.error(error.message);
}
与前面带有 setTimeout 的示例一样,传递给 addEventListener 的任何回调均 异步 执行:
Track A: --> try/catch
Track B: --> addEventListener --> callback --> throw

如果我们不希望程序崩溃,则要正确处理错误,我们必须在 addEventListener 的回调中移动 try/catch。但同样,这样做几乎没有任何价值。

与 setTimeout 一样,异步代码路径抛出的异常 无法从外部捕获,这将使程序崩溃。

在下一部分中,我们将了解如何使用 Promises 和 async/await 简化异步代码的错误处理。

onerror 是什么情况?

HTML 元素有许多事件处理器,例如 onclick、onmouseenter、onchange 等。

还有 onerror,但它与 throw 之类是无关的。

每当<img>标签或<script>之类的 HTML 元素遇到不存在的资源时,onerror 事件处理器都会触发。

考虑以下示例:
// omitted
<body>
<img src="nowhere-to-be-found.png" alt="So empty!">
</body>
// omitted

当访问缺少资源或不存在资源的 HTML 文档时,浏览器的控制台会记录以下错误:

GET http://localhost:5000/nowhere-to-be-found.png
[HTTP/1.1 404 Not Found 3ms]
在 JavaScript 中,我们可以使用适当的事件处理器来“捕获”此错误:
const image = document.querySelector("img");
image.onerror = function(event) {
  console.log(event);
};
更好的是:
const image = document.querySelector("img");
image.addEventListener("error", function(event) {
  console.log(event);
});

此模式可以方便地 加载替代资源来代替丢失的图像或脚本。但是请记住:onerror 与 throw 或 try/catch 无关。

使用 Promise 处理错误
为了说明用 Promise 处理错误的机制,我们将“Promise”我们的一个原始示例。调整以下函数:
function toUppercase(string) {
  if (typeof string !== "string") {
    throw TypeError("Wrong type given, expected a string");
  }
  return string.toUpperCase();
}
toUppercase(4);
这里我们不会返回简单的字符串或异常,而是分别使用 Promise.reject 和 Promise.resolve 处理错误和成功情况:
function toUppercase(string) {
  if (typeof string !== "string") {
    return Promise.reject(TypeError("Wrong type given, expected a string"));
  }
  const result = string.toUpperCase();
  return Promise.resolve(result);
}

(从技术上讲这段代码中没有异步的内容,但它很好地展示了具体的机制)。

现在函数已“Promise 化”,我们可以附加 then 来消费结果,并 catch 以 处理被拒绝的 Promise
toUppercase(99)
  .then(result => result)
  .catch(error => console.error(error.message));
代码会记录:
Wrong type given, expected a string

在 Promise 的世界中,catch 是用于处理错误的结构。除了 catch 和 then,我们也有 finally,类似于 try/catch 中的 finally。

作为其同步的“相对”,Promise 的 finally无论Promise 的结果如何都会运行:
toUppercase(99)
  .then(result => result)
  .catch(error => console.error(error.message))
  .finally(() => console.log("Run baby, run"));

谨记,传递给 then/catch/finally 的任何回调都是由微任务队列异步处理的。它们是 微任务,优先于事件和计时器等宏任务。

Promise,错误和抛出
作为 拒绝 Promise 的最佳实践,提供错误对象很方便:
Promise.reject(TypeError("Wrong type given, expected a string"));

这样,你可以在代码库中保持错误处理的一致性。其他团队成员总是能访问 error.message,更重要的是你可以检查堆栈跟踪。除了 Promise.reject,我们还可以通过 抛出 异常来退出 Promise 链。

考虑以下示例:
Promise.resolve("A string").then(value => {
  if (typeof value === "string") {
    throw TypeError("Expected a number!");
  }
});
我们用一个字符串解析一个 Promise,然后 Promise 链会立刻被 throw 断开。要停止异常传播,我们照常使用 catch:
Promise.resolve("A string")
  .then(value => {
    if (typeof value === "string") {
      throw TypeError("Expected a number!");
    }
  })
  .catch(reason => console.log(reason.message));
这种模式在 fetch 中很常见,我们在 fetch 中检查响应对象以查找错误:
fetch("https://example-dev/api/")
  .then(response => {
    if (!response.ok) {
      throw Error(response.statusText);
    }
    return response.json();
  })
  .then(json => console.log(json));

在这里,异常可以被 catch 拦截。如果我们失败了,或者决定不在这里捕获它,那么 异常就可以在堆栈中冒泡了。这本身并不坏,但是不同的环境对未捕获的拒绝的反应是不同的。

例如,将来的 Node.js 将使任何未处理 Promise 拒绝的程序崩溃:
DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

所以最好捕获它们!

“Promise 化”计时器的错误处理
使用计时器或事件无法捕获从回调抛出的异常。我们在上一节中看到了一个示例:
function failAfterOneSecond() {
  setTimeout(() => {
    throw Error("Something went wrong!");
  }, 1000);
}
// DOES NOT WORK
try {
  failAfterOneSecond();
} catch (error) {
  console.error(error.message);
}
Promise 提供的一个解决方案是代码的“Promise 化”。具体来说,我们用 Promise 包装计时器:
function failAfterOneSecond() {
  return new Promise((_, reject) => {
    setTimeout(() => {
      reject(Error("Something went wrong!"));
    }, 1000);
  });
}
通过 reject,我们启动了一个 Promise 拒绝,带有一个错误对象。这时,我们可以使用 catch 处理异常:
failAfterOneSecond().catch(reason => console.error(reason.message));

注意:通常使用 value 作为 Promise 的返回值,并使用 reason 作为拒绝的返回对象。

Node.js 有一个名为 promisify 的实用程序,可简化旧式回调 API 的“Promise 化”。

https://nodejs.org/api/util.html#util_util_promisify_original

Promise.all 中的错误处理
静态方法 Promise.all 接收一个 Promise 数组,并从所有解析中的 Promise 返回一个结果数组:
const promise1 = Promise.resolve("All good!");
const promise2 = Promise.resolve("All good here too!");
Promise.all([promise1, promise2]).then((results) => console.log(results));
// [ 'All good!', 'All good here too!' ]
如果这些 Promise 中的任何一个被拒绝,Promise.all 都会拒绝,并返回第一个被拒绝的 Promise 中的错误。为了在 Promise.all 中处理这些情况,我们像上一节中一样使用 catch:
const promise1 = Promise.resolve("All good!");
const promise2 = Promise.reject(Error("No good, sorry!"));
const promise3 = Promise.reject(Error("Bad day ..."));
Promise.all([promise1, promise2, promise3])
  .then(results => console.log(results))
  .catch(error => console.error(error.message));
同样,无论 Promise.all 的结果如何都要运行函数的话,我们可以使用 finally:
Promise.all([promise1, promise2, promise3])
  .then(results => console.log(results))
  .catch(error => console.error(error.message))
  .finally(() => console.log("Always runs!"));
Promise.any 中的错误处理

我们可以将 Promise.any(Firefox>79,Chrome>85)视为 Promise.all 的反面。

即使数组中只有一个 Promise 拒绝,Promise.all 也会返回失败;而 Promise.any 始终为我们提供第一个已解析的 Promise(如果存在于数组中),不管发生了什么拒绝。

如果 所有 传递给 Promise.any 的 Promise 都拒绝,则产生的错误是 AggregateError。考虑以下示例:
Promise.all([promise1, promise2, promise3])
  .then(results => console.log(results))
  .catch(error => console.error(error.message))
  .finally(() => console.log("Always runs!"));
在这里,我们使用 catch 处理错误。此代码的输出是:
const promise1 = Promise.reject(Error("No good, sorry!"));
const promise2 = Promise.reject(Error("Bad day ..."));
Promise.any([promise1, promise2])
  .then(result => console.log(result))
  .catch(error => console.error(error))
  .finally(() => console.log("Always runs!"));
AggregateError 对象具有与基础的 Error 相同的属性,外加一个 errors 属性:
AggregateError: No Promise in Promise.any was resolved
Always runs!

此属性是由拒绝产生的各个错误组成的数组:

//
  .catch(error => console.error(error.errors))
//
Promise.race 中的错误处理
静态方法 Promise.race 接收一个 Promise 数组:
[Error: "No good, sorry!, Error: "Bad day ..."]
结果是 第一个赢得“比赛”的 Promise。那拒绝呢?如果拒绝的 Promise 不是第一个出现在输入数组中的对象,则 Promise.race 解析:
const promise1 = Promise.resolve("The first!");
const promise2 = Promise.resolve("The second!");
Promise.race([promise1, promise2]).then(result => console.log(result));
// The first!
如果 拒绝出现在数组的第一个元素中,则 Promise.race 拒绝,且我们必须捕获这个拒绝:
const promise1 = Promise.resolve("The first!");
const rejection = Promise.reject(Error("Ouch!"));
const promise2 = Promise.resolve("The second!");
Promise.race([rejection, promise1, promise2])
  .then(result => console.log(result))
  .catch(error => console.error(error.message));
// Ouch!
Promise.allSettled 中的错误处理

Promise.allSettled 是 ECMAScript 2020 加入的。

使用这种静态方法没有什么要处理的,因为 即使一个或多个输入 Promise 拒绝,结果始终是一个已解析的 Promise

考虑以下示例:
const promise1 = Promise.resolve("Good!");
const promise2 = Promise.reject(Error("No good, sorry!"));
Promise.allSettled([promise1, promise2])
  .then(results => console.log(results))
  .catch(error => console.error(error))
  .finally(() => console.log("Always runs!"));

我们传递给 Promise.allSettled 一个由两个 Promise 组成的数组:一个已解析,另一个被拒绝。在这种情况下,catch 将永远不会启用。于是会运行 finally。

代码的结果记录在 then 中,如下:
[
  { status: 'fulfilled', value: 'Good!' },
  {
    status: 'rejected',
    reason: Error: No good, sorry!
  }
]
async/await 的错误处理

JavaScript 中的 async/await 表示异步函数,但是从读者的角度来看,它们也拥有同步函数的所有 可读性

为简单起见,我们将先前的同步函数设为 Uppercase,并在 function 关键字之前放置 async,以将其转换为异步函数:
async function toUppercase(string) {
  if (typeof string !== "string") {
    throw TypeError("Wrong type given, expected a string");
  }
  return string.toUpperCase();
}
只需在函数前面加上 async 前缀,我们就可以使函数 返回一个 Promise。这意味着我们可以在函数调用之后来一串 then、catch 和 finally:
async function toUppercase(string) {
  if (typeof string !== "string") {
    throw TypeError("Wrong type given, expected a string");
  }
  return string.toUpperCase();
}
toUppercase("abc")
  .then(result => console.log(result))
  .catch(error => console.error(error.message))
  .finally(() => console.log("Always runs!"));

当我们从一个 async 函数中抛出异常时,异常将成为底层 Promise 被拒绝的原因

可以使用 catch 从外部拦截任何错误。

最重要的是,除了这种样式外,我们还可以使用try/catch/finally,就像我们使用同步函数时所做的一样。

在下面的示例中,我们从另一个函数 consumer 调用 toUppercase,前者方便地用 try/catch/finally 将函数调用包装起来:
async function toUppercase(string) {
  if (typeof string !== "string") {
    throw TypeError("Wrong type given, expected a string");
  }
  return string.toUpperCase();
}
async function consumer() {
  try {
    await toUppercase(98);
  } catch (error) {
    console.error(error.message);
  } finally {
    console.log("Always runs!");
  }
}
consumer(); // Returning Promise ignored
输出是:
Wrong type given, expected a string
Always runs!

同一主题的资料:如何从 JavaScript 中的 async 函数抛出错误?

https://www.valentinog.com/blog/throw-async/

异步生成器的错误处理

JavaScript 中的 异步生成器能够生成 Promise 而非简单值的生成器函数

它们将生成器函数与 async 结合在一起。结果是一个生成器函数,其迭代器对象将一个 Promise 暴露给消费者。

要创建一个异步生成器,我们用星号 * 声明一个生成器函数,加一个 async 前缀:
async function* asyncGenerator() {
  yield 33;
  yield 99;
  throw Error("Something went wrong!"); // Promise.reject
}

此处的错误处理规则也是和 Promise 一样的。在异步生成器中 throw 将导致一个 Promise 拒绝,我们使用 catch 拦截它。要从异步生成器拉出 Promise,我们可以使用两种方法:

  • then 处理器。

  • async 迭代

从上面的示例中,我们可以肯定地知道在前两个 yield 之后会有一个异常。也就是说我们可以:
const go = asyncGenerator();
go.next().then(value => console.log(value));
go.next().then(value => console.log(value));
go.next().catch(reason => console.error(reason.message));
代码输出是:
{ value: 33, done: false }
{ value: 99, done: false }
Something went wrong!

另一种方法是使用 for await...of 的 async 迭代。要使用 async 迭代,我们需要使用一个 async 函数包装这个消费者。

下面是完整的示例:
async function* asyncGenerator() {
  yield 33;
  yield 99;
  throw Error("Something went wrong!"); // Promise.reject
}
async function consumer() {
  for await (const value of asyncGenerator()) {
    console.log(value);
  }
}
consumer();
与 async/await 一样,我们使用 try/catch 处理任何潜在的异常:
async function* asyncGenerator() {
  yield 33;
  yield 99;
  throw Error("Something went wrong!"); // Promise.reject
}
async function consumer() {
  try {
    for await (const value of asyncGenerator()) {
      console.log(value);
    }
  } catch (error) {
    console.error(error.message);
  }
}
consumer();
代码输出是:
33
99
Something went wrong!
从异步生成器函数返回的迭代器对象也有一个 throw() 方法,非常像它的同步形式。在此处的迭代器对象上调用 throw() 不会抛出异常,而是一个 Promise 拒绝:
async function* asyncGenerator() {
  yield 33;
  yield 99;
  yield 11;
}
const go = asyncGenerator();
go.next().then(value => console.log(value));
go.next().then(value => console.log(value));
go.throw(Error("Let's reject!"));
go.next().then(value => console.log(value)); // value is undefined
要从外部处理这种情况,我们可以执行以下操作:
go.throw(Error("Let's reject!")).catch(reason => console.error(reason.message));
但是,请不要忘记迭代器对象 throw()在生成器内部 发送异常。也就是说我们还可以应用以下模式:
async function* asyncGenerator() {
  try {
    yield 33;
    yield 99;
    yield 11;
  } catch (error) {
    console.error(error.message);
  }
}
const go = asyncGenerator();
go.next().then(value => console.log(value));
go.next().then(value => console.log(value));
go.throw(Error("Let's reject!"));
go.next().then(value => console.log(value)); // value is undefined
Node.js 中的错误处理
Node.js 中的同步错误处理

Node.js 中的同步错误处理与上文介绍的内容并没有太大差异。

对于 同步代码,try/catch/finally 没什么问题。

但如果我们进入异步世界,事情就会变得很有趣了。

Node.js 中的异步错误处理:回调模式

对于异步代码,Node.js 强烈依赖两个习惯用法:

  • 回调模式。

  • 事件发射器。

回调模式 中,异步 Node.jsAPI 接收一个函数,该函数通过 事件循环 处理,并在 调用堆栈 为空时立即执行。

考虑以下代码:
const { readFile } = require("fs");
function readDataset(path) {
  readFile(path, { encoding: "utf8" }, function(error, data) {
    if (error) console.error(error);
    // do stuff with the data
  });
}
如果从此清单中提取回调,就可以看到错误的处理方式:
//
function(error, data) {
    if (error) console.error(error);
    // do stuff with the data
  }
//

如果使用 fs.readFile 读取给定路径时引发任何错误,我们将获得一个错误对象。这时我们可以:

  • 像之前一样简单地记录错误对象。

  • 抛出一个异常。

  • 将这个错误传递给另一个回调。

要抛出异常,我们可以执行以下操作:
const { readFile } = require("fs");
function readDataset(path) {
  readFile(path, { encoding: "utf8" }, function(error, data) {
    if (error) throw Error(error.message);
    // do stuff with the data
  });
}
但是,与 DOM 中的事件和计时器一样,此异常将 使程序崩溃。尝试使用 try/catch 停止它的方法将不起作用:
const { readFile } = require("fs");
function readDataset(path) {
  readFile(path, { encoding: "utf8" }, function(error, data) {
    if (error) throw Error(error.message);
    // do stuff with the data
  });
}
try {
  readDataset("not-here.txt");
} catch (error) {
  console.error(error.message);
}
如果我们不想使程序崩溃,则将错误传递给另一个回调是首选方法
const { readFile } = require("fs");
function readDataset(path) {
  readFile(path, { encoding: "utf8" }, function(error, data) {
    if (error) return errorHandler(error);
    // do stuff with the data
  });
}
顾名思义,errorHandler 是一个用于错误处理的简单函数:
function errorHandler(error) {
  console.error(error.message);
  // do something with the error:
  // - write to a log.
  // - send to an external logger.
}
Node.js 中的异步错误处理:事件发射器

你在 Node.js 中所做的大部分工作都是基于 事件 的。大多数情况下,你会与 发射器对象 和一些观察者交互以侦听消息。

Node.js 中的任何事件驱动模块(例如 net,https://nodejs.org/dist/latest-v14.x/docs/api/net.html)都扩展了一个名为 EventEmitter 的根类。

Node.js 中的 EventEmitter 有两种基本方法:on 和 emit。

考虑以下简单的 HTTP 服务器:
const net = require("net");
const server = net.createServer().listen(8081, "127.0.0.1");
server.on("listening", function () {
  console.log("Server listening!");
});
server.on("connection", function (socket) {
  console.log("Client connected!");
  socket.end("Hello client!");
});

在这里我们监听两个事件:listening 和 connection。除了这些事件之外,事件发射器还在出现错误时公开一个 错误 事件。

如果你在端口 80 上运行此代码,则会得到一个异常:
const net = require("net");
const server = net.createServer().listen(80, "127.0.0.1");
server.on("listening", function () {
  console.log("Server listening!");
});
server.on("connection", function (socket) {
  console.log("Client connected!");
  socket.end("Hello client!");
});
输出:
events.js:291
      throw er; // Unhandled 'error' event
      ^
Error: listen EACCES: permission denied 127.0.0.1:80
Emitted 'error' event on Server instance at: ...
要捕获它,我们可以注册一个 错误 事件处理器:
server.on("error", function(error) {
  console.error(error.message);
});
这会打印:
listen EACCES: permission denied 127.0.0.1:80

此外,该程序不会崩溃。要了解有关该主题的更多信息,请参考“Node.js 中的错误处理”。

https://www.joyent.com/node-js/production/design/errors

    总结    

在本指南中,我们涵盖了从简单同步代码到高级异步原语的 JavaScript 错误处理完整概念

在我们的 JavaScript 程序中,可以通过多种方式来显示异常。

同步代码中的异常是最容易捕获的。相反,异步代码 路径中的 异常 可能很难处理。

同时,浏览器中的新 JavaScript API 几乎都通向 Promise。这种普遍的模式使我们更容易用 then/catch/finally 或 try/catch 对 async/await 处理异常。

阅读本指南后,你应该能够 识别程序中可能出现的所有不同情况,并正确捕获 异常

感谢你的阅读和关注!

延伸阅读

https://www.valentinog.com/blog/error/

活动推荐

如果说微服务是解锁系统复杂化的“钥匙”,那 IoT 就是构建系统智能化的有效工具,如何正确落地应用?9 月 19 日 13:00-17:00DevRun 开发者沙龙华为云杭州专场从实践出发带你解锁答案,现场还有实操演练,礼品多多不容错过~ 点击阅读原文或扫码抓紧报名吧!

推荐阅读