写点什么

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:181885
用户头像
小智 前 InfoQ 主编

发布了 400 篇内容, 共 318.5 次阅读, 收获喜欢 1762 次。

关注

评论

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

CentOS安装Docker运行环境

wjchenge

Docker Centos 7

拒不外传!阿里内部耗重金找人总结出这份并发编程手册(全彩版)

周老师

Java 编程 程序员 架构 面试

工作中,有哪些SQL是我们必须要掌握的?

xiezhr

oracle sql SQL语法 3月日更

农田治理效率低下还赔本?智慧农业力保粮食品质,效率事半功倍

一只数据鲸鱼

物联网 数据可视化 智慧城市 智慧农业 农业管理

萌新不看会后悔的C++基本类型总结(一)

花狗Fdog

如何解决移动直播下的耳返延迟问题

融云 RongCloud

音视频 移动直播

企业级链表设计思路:

大忽悠

3月日更

腾讯T2大牛手把手教你!2021新一波程序员跳槽季,算法太TM重要了

欢喜学安卓

android 程序员 面试 移动开发

凡尔赛一波:开年闭关2月从小厂跳槽逆袭,成功入职美团(面经+心得)

程序员小毕

Java 程序员 面试 跳槽

【得物技术】会议室巡检系统(哮天犬)部署分享

得物技术

分享 部署 巡检 得物技术 会议室

微服务指南

信码由缰

DevOps 微服务架构

Nginx 模块系统:前篇

soulteary

nginx 动态模块

华山版强势来袭!阿里巴巴Java性能优化2021年3月版(面试必备)

Java架构追梦

Java 阿里巴巴 架构 面试 性能优化

详细!蚂蚁、字节、PDD社招面经Java岗(分布式+线程安全+MySQL+CAS)

Java成神之路

Java 程序员 架构 面试 编程语言

力扣(LeetCode)刷题,简单+中等题(第32期)

不脱发的程序猿

算法 LeetCode 编程能力 28天写作 3月日更

金三银四如何突击面试美团?面试题(含答案)+学习笔记+电子书籍+学习视频

比伯

Java 编程 架构 面试 程序人生

Pano React Native SDK 来了!快速实现移动端音视频和白板

拍乐云Pano

flutter ios android RTC React Native

百亿级流量的百度搜索中台,是怎么做可观测性建设的?

百度Geek说

中台 云原生 #百度#

酷睿i7-10870H对比锐龙7 5800H游戏性能, 英特尔仍是游戏本CPU的更优选

intel001

干货来袭!2021面试必备阿里巴巴Java性能优化速成笔记我粉了!

Java王路飞

Java 程序员 架构 面试 性能优化

程序员之禅(三)

每天读本书

每天读本书

一年半工作经历的我是怎么字节四面成功拿到offer的?

Java成神之路

Java 程序员 架构 面试 编程语言

字节资深面试官带你深度剖析:Java面试技术点+互联网面试真题解析

Java成神之路

Java 程序员 架构 面试 编程语言

重磅!Flutter中网络图片加载和缓存源码分析,BAT大厂面试总结

欢喜学安卓

android 程序员 面试 移动开发

想看新指标?教你轻松写prober插件

Obsuite

运维 滴滴夜莺 Obsuite prober插件

【LeetCode】用栈实现队列Java题解

HQ数字卡

算法 LeetCode 28天写作

英特尔:i7-10870H 游戏性能超 R7 5800H,更强的 11 代酷睿 H 在后面

intel001

中国程序员最容易发错的单词

happlyfox

GitHub 学习 程序人生 3月日更

大赛报名|首次聚焦口罩场景!第三届 106 点关键点定位大赛开启

京东科技开发者

人工智能 深度学习 计算机视觉

CodeHub#4 启动报名| 荷小鱼:K12 在线教育应用的开发实践

蚂蚁集团移动开发平台 mPaaS

在线教育 mPaaS codehub 离线包

当AI开始改造“文房四宝”:腾讯教育的脑洞与逻辑

脑极体

边缘计算隔离技术的挑战与实践

边缘计算隔离技术的挑战与实践

JavaScript错误处理完全指南-InfoQ