提前锁票 InfoQ 最具价值感的视频栏目 | InfoQ 大咖说 了解详情
写点什么

JavaScript 错误处理完全指南

2020 年 9 月 17 日

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 上的这一页面。


什么是异常?

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


要在 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; // 33const secondStep = go.next().value; // 99
复制代码


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


使用这种方法,我们可以将异常注入生成器来暂停程序:


function* generate() {  yield 33;  yield 99;}const go = generate();const firstStep = go.next().value; // 33go.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:3399Tired 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/catchTrack 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/catchTrack 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 WORKtry {  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 resolvedAlways 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 stringAlways 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();
复制代码


代码输出是:


3399Something 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)都扩展了一个名为 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:80Emitted 'error' event on Server instance at: ...
复制代码


要捕获它,我们可以注册一个 错误 事件处理器:


server.on("error", function(error) {  console.error(error.message);});
复制代码


这会 Print:


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 处理异常。


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


感谢你的阅读和关注!


英文原文

A mostly complete guide to error handling in JavaScript


2020 年 9 月 17 日 16:181739
用户头像
小智 前 InfoQ 主编

发布了 399 篇内容, 共 312.3 次阅读, 收获喜欢 1737 次。

关注

评论

发布
暂无评论
发现更多内容

Week04总结

熊威

使用图解的方式来解决链表的算法问题

jerry.mei

Java 算法 链表 ARTS 打卡计划 js

第四周学习总结

铁血杰克

架构师训练营第四周作业

王铭铭

如何建设一个典型互联网应用系统

已昏懒人

架构 架构师 极客大学架构师训练营 架构思维

架构师训练营第四周学习总结

Bruce Xiong

高流量秒杀系统的优化思路

铁血杰克

04-02学习总结

ruettiger

作业:一个典型的大型互联网架构演进采用的技术

蒜泥精英

不会用这个远控工具 怎么好意思说你会远程运维?

InfoQ_21c8aba5317f

远控工具

架构师训练营 - 学习总结 第 4 周

水边

极客大学架构师训练营

大型互联网系统会面对怎样的一些挑战

Acker飏

架构师训练营第四周总结

James-Pang

极客大学架构师训练营

第四周作业一

carol

案例分享

大型互联网站技术猜想

Dawn

极客大学架构师训练营

互联网系统架构的演进-笔记心得

蒜泥精英

大型互联网系统应用了哪些技术

elfkingw

极客大学架构师训练营

永中云转换助力教育行业文档在线预览更高效

InfoQ_21c8aba5317f

行业资讯 永中

【架构训练 Week04 作业】

Rex

架构模式:可复用的架构问题解决方案

迷失的月亮

架构模式 极客大学架构师训练营

第四周总结

晨光

第四周作业

魔曦

极客大学架构师训练营

架构师训练营第四周作业

James-Pang

极客大学架构师训练营

第四周作业

晨光

ARTS 03 - 使用图解的方式来解决链表的算法问题

jerry.mei

算法 前端 练习 ARTS 打卡计划 ES6

redis设计与实现(1)redis数据结构

程序员老王

redis

互联网面临的挑战

师哥

架构师训练营第四章总结

吴吴

04-作业01

ruettiger

第四周-作业1

seng man

架构师训练营第四章作业

吴吴

打造 VUCA 时代的 10 倍速 IT 团队

打造 VUCA 时代的 10 倍速 IT 团队

JavaScript错误处理完全指南-InfoQ