JavaScript错误处理完整指南

发表于 2年以前  | 总阅读数:552 次

本文将带你了解 JavaScript 中常见的错误类型,处理同步和异步 JavaScript/Node.js 代码中错误和异常的方式,以及错误处理最佳实践!

1 . 错误概述

JavaScript 中的错误是一个对象,在发生错误时会抛出该对象以停止程序。在 JavaScript 中,可以通过构造函数来创建一个新的通用错误:

const err = new Error("Error");

当然,也可以省略 new 关键字:

const err = Error("Error");

Error 对象有三个属性:

  • message:带有错误消息的字符串;
  • name: 错误的类型;
  • stack:函数执行的堆栈跟踪。

例如,创建一个 TypeError 对象,该消息将携带实际的错误字符串,其 name 将是“TypeError”:

const wrongType = TypeError("Expected number");

wrongType.message; // 'Expected number'
wrongType.name;    // 'TypeError'

堆栈跟踪是发生异常或警告等事件时程序所处的方法调用列表:

它首先会打印错误名称和消息,然后是被调用的方法列表。每个方法调用都说明其源代码的位置和调用它的行。可以使用此数据来浏览代码库并确定导致错误的代码段。此方法列表以堆叠的方式排列。它显示了异常首先被抛出的位置以及它如何通过堆栈方法调用传播。为异常实施捕获不会让它通过堆栈向上传播并使程序崩溃。

对于 Error 对象,Firefox 还实现了一些非标准属性:

  • columnNumber:错误所在行的列号;
  • filename:发生错误的文件
  • lineNumber:发生错误的行号

2 . 错误类型

JavaScript 中有一系列预定义的错误类型。只要使用者没有明确处理应用程序中的错误,它们就会由 JavaScript 运行时自动选择和定义。

JavaScript中的错误类型包括:

  • EvalError
  • InternalError
  • RangeError
  • ReferenceError
  • SyntaxError
  • TypeError
  • URIError

这些错误类型都是实际的构造函数,旨在返回一个新的错误对象。最常见的就是 TypeError。大多数时候,大部分错误将直接来自 JavaScript 引擎,例如 InternalError 或 SyntaxError。

JavaScript 提供了 instanceof 运算符可以用于区分异常类型:

try {
  If (typeof x !== ‘number’) {
       throw new TypeError(‘x 应是数字’);
  } else if (x <= 0) {
       throw new RangeError('x 应大于 0');
  } else {
       // ...
  }
} catch (err) {
    if (err instanceof TypeError) {
      // 处理 TypeError 错误
    } else if (err instanceof RangeError) {
      // 处理 RangeError 错误
  } else {
      // 处理其他类型错误
  }
}

下面来了解 JavaScript 中最常见的错误类型,并了解它们发生的时间和原因。

(1)SyntaxError

SyntaxError 表示语法错误。这些错误是最容易修复的错误之一,因为它们表明代码语法中存在错误。由于 JavaScript 是一种解释而非编译的脚本语言,因此当应用程序执行包含错误的脚本时会抛出这些错误。在编译语言的情况下,此类错误在编译期间被识别。因此,在修复这些问题之前,不会创建应用程序二进制文件。

SyntaxError 发生的一些常见原因是:

  • 缺少引号
  • 缺少右括号
  • 大括号或其他字符对齐不当

(2)TypeError

TypeError 是 JavaScript 应用程序中最常见的错误之一,当某些值不是特定的预期类型时,就会产生此错误。 TypeError 发生的一些常见原因是:

  • 调用不是方法的对象。
  • 试图访问 null 或未定义对象的属性
  • 将字符串视为数字,反之亦然

(3)ReferenceError

ReferenceError 表示引用错误。当代码中的变量引用有问题时,会发生 ReferenceError。可能忘记在使用变量之前为其定义一个值,或者可能试图在代码中使用一个不可访问的变量。在任何情况下,通过堆栈跟踪都可以提供充足的信息来查找和修复有问题的变量引用。

ReferenceErrors 发生的一些常见原因如下:

  • 在变量名中输入错误。
  • 试图访问其作用域之外的块作用域变量。
  • 在加载之前从外部库引用全局变量。

(4)RangeError

RangeError 表示范围错误。当变量设置的值超出其合法值范围时,将抛出 RangeError。它通常发生在将值作为参数传递给函数时,并且给定值不在函数参数的范围内。当使用记录不完整的第三方库时,有时修复起来会很棘手,因为需要知道参数的可能值范围才能传递正确的值。

RangeError 发生的一些常见场景如下:

  • 试图通过 Array 构造函数创建非法长度的数组。
  • 将错误的值传递给数字方法,例如 toExponential()toPrecision()toFixed() 等。
  • 将非法值传递给字符串函数,例如 normalize()

(5)URIError

URIError 表示 URI错误。当 URI 的编码和解码出现问题时,会抛出 URIError。JavaScript 中的 URI 操作函数包括:decodeURIdecodeURIComponent 等。如果使用了错误的参数(无效字符),就会抛出 URIError。

(6)EvalError

EvalError 表示 Eval 错误。当 eval() 函数调用发生错误时,会抛出 EvalError。不过,当前的 JavaScript 引擎或 ECMAScript 规范不再抛出此错误。但是,为了向后兼容,它仍然是存在的。

如果使用的是旧版本的 JavaScript,可能会遇到此错误。在任何情况下,最好调查在eval()函数调用中执行的代码是否有任何异常。

(7)InternalError

InternalError 表示内部错误。在 JavaScript 运行时引擎发生异常时使用。它表示代码可能存在问题也可能不存在问题。

InternalError 通常只发生在两种情况下:

  • 当 JavaScript 运行时的补丁或更新带有引发异常的错误时(这种情况很少发生);
  • 当代码包含对于 JavaScript 引擎而言太大的实体时(例如,数组初始值设定项太大、递归太多)。

解决此错误最合适的方法就是通过错误消息确定原因,并在可能的情况下重构应用逻辑,以消除 JavaScript 引擎上工作负载的突然激增。

注意: 现代 JavaScript 中不会抛出 EvalError 和 InternalError。

(8)创建自定义错误类型

虽然 JavaScript 提供了足够的错误类型类列表来涵盖大多数情况,但如果这些错误类型不能满足要求,还可以创建新的错误类型。这种灵活性的基础在于 JavaScript 允许使用 throw 命令抛出任何内容。

可以通过扩展 Error 类以创建自定义错误类:

class ValidationError extends Error {
    constructor(message) {
        super(message);
        this.name = "ValidationError";
    }
}

可以通过以下方式使用它:

throw ValidationError("未找到该属性: name")

可以使用 instanceof 关键字识别它:

try {
    validateForm() // 抛出 ValidationError 的代码
} catch (e) {
    if (e instanceof ValidationError) {

    }
    else {

    }
}

3 . 抛出错误

很多人认为错误和异常是一回事。实际上,Error 对象只有在被抛出时才会成为异常

在 JavaScript 中抛出异常,可以使用 throw 来抛出 Error 对象:

throw TypeError("Expected number");

或者:

throw new TypeError("Expected number");

来看一个简单的例子:

function toUppercase(string) {
  if (typeof string !== "string") {
    throw TypeError("Expected string");
  }

  return string.toUpperCase();
}

在这里,我们检查函数参数是否为字符串。如果不是,就抛出异常。

从技术上讲,我们可以在 JavaScript 中抛出任何东西,而不仅仅是 Error 对象:

throw Symbol();
throw 33;
throw "Error!";
throw null;

但是,最好避免这样做:要抛出正确的 Error 对象,而不是原语

4 . 抛出异常时会发生什么?

异常一旦抛出,就会在程序堆栈中冒泡,除非在某个地方被捕获。

来看下面的例子:

function toUppercase(string) {
  if (typeof string !== "string") {
    throw TypeError("Expected string");
  }

  return string.toUpperCase();
}

toUppercase(4);

在浏览器或 Node.js 中运行此代码,程序将停止并抛出错误:

这里还显示了发生错误的确切行。这个错误就是一个堆栈跟踪,有助于跟踪代码中的问题。堆栈跟踪从下到上:

at toUppercase (<anonymous>:3:11)
at <anonymous>:9:1

toUppercase 函数在第 9 行调用,在第 3 行抛出错误。除了在浏览器的控制台中查看此堆栈跟踪之外,还可以在 Error 对象的 stack 属性上访问它。

介绍完这些关于错误的基础知识之后,下面来看看同步和异步 JavaScript 代码中的错误和异常处理。

5 . 同步错误处理

(1)常规函数的错误处理

同步代码会按照代码编写顺序执行。让我们再看看前面的例子:

function toUppercase(string) {
  if (typeof string !== "string") {
    throw TypeError("Expected string");
  }

  return string.toUpperCase();
}

toUppercase(4);

在这里,引擎调用并执行 toUppercase,这一切都是同步发生的。 要捕获由此类同步函数引发的异常,可以使用 try/catch/finally:

try {
  toUppercase(4);
} catch (error) {
  console.error(error.message);
} finally {
  // ...
}

通常,try 会处理正常的路径,或者可能进行的函数调用。catch 就会捕获实际的异常,它接收 Error 对象。而不管函数的结果如何,finally 语句都会运行:无论它失败还是成功,finally 中的代码都会运行。

(2)生成器函数的错误处理

JavaScript 中的生成器函数是一种特殊类型的函数。它可以随意暂停和恢复,除了在其内部范围和消费者之间提供双向通信通道。为了创建一个生成器函数,需要在 function 关键字后面加上一个 *

function* generate() {
//
}

只要进入函数,就可以使用 yield 来返回值:

function* generate() {
  yield 33;
  yield 99;
}

生成器函数的返回值是一个迭代器对象。要从生成器中提取值,可以使用两种方法:

  • 在迭代器对象上调用 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 将代码包装在生成器中:

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);
}

输出结果如下: 这里,try 块中包含正常的迭代。如果发生任何异常,就会用 catch 捕获它。

6 . 异步错误处理

浏览器中的异步包括定时器、事件、Promise 等。异步世界中的错误处理与同步世界中的处理不同。下面来看一些例子。

(1)定时器的错误处理

上面我们介绍了如何使用 try/catch/finally 来处理错误,那异步中可以使用这些来处理错误吗?先来看一个例子:

function failAfterOneSecond() {
  setTimeout(() => {
    throw Error("Wrong!");
  }, 1000);
}

此函数在大约 1 秒后会抛出错误。那处理此异常的正确方法是什么?以下代码是无效的:

function failAfterOneSecond() {
  setTimeout(() => {
    throw Error("Wrong!");
  }, 1000);
}

try {
  failAfterOneSecond();
} catch (error) {
  console.error(error.message);
}

我们知道,try/catch是同步的,所以没办法这样来处理异步中的错误。当传递给 setTimeout的回调运行时,try/catch 早已执行完毕。程序将会崩溃,因为未能捕获异常。它们是在两条路径上执行的:

A: --> try/catch
B: --> setTimeout --> callback --> throw

(2)事件的错误处理

我们可以监听页面中任何 HTML 元素的事件,DOM 事件的错误处理机制遵循与任何异步 Web API 相同的方案。

来看下面的例子:

const button = document.querySelector("button");

button.addEventListener("click", function() {
  throw Error("error");
});

这里,在单击按钮后立即抛出了异常,我们该如何捕获这个异常呢?这样写是不起作用的,也不会阻止程序崩溃:

const button = document.querySelector("button");

try {
  button.addEventListener("click", function() {
    throw Error("error");
  });
} catch (error) {
  console.error(error.message);
}

与前面的 setTimeout 例子一样,任何传递给 addEventListener 的回调都是异步执行的:

Track A: --> try/catch
Track B: --> addEventListener --> callback --> throw

如果不想让程序崩溃,为了正确处理错误,就必须将 try/catch 放到 addEventListener 的回调中。不过这样做并不是最佳的处理方式,与 setTimeout 一样,异步代码路径抛出的异常无法从外部捕获,并且会使程序崩溃。

下面会介绍 Promises 和 async/await 是如何简化异步代码的错误处理的。

(3)onerror

HTML 元素有许多事件处理程序,例如 onclickonmouseenteronchange 等。除此之外,还有 onerror,每当 <img> 标签或 <script> 等 HTML 元素命中不存在的资源时,onerror 事件处理程序就会触发。

来看下面的例子:

<body>
  <img src="nowhere-to-be-found.png">
</body>

当访问的资源缺失时,浏览器的控制台就会报错:

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);
};

或者使用 addEventListener 来监听 error 事件,当发生错误时进行处理:

const image = document.querySelector("img");

image.addEventListener("error", function(event) {
  console.log(event);
});

此模式对于加载备用资源以代替丢失的图像或脚本很有用。不过需要记住:onerror 与 throw 或 try/catch 是无关的。

(4)Promise 的错误处理

下面来通过最上面的 toUppercase 例子看看 Promise 是如何处理错误的:

function toUppercase(string) {
  if (typeof string !== "string") {
    throw TypeError("Expected string");
  }

  return string.toUpperCase();
}

toUppercase(4);

对上面的代码进行修改,不返回简单的字符串或异常,而是分别使用 Promise.rejectPromise.resolve 来处理错误和成功:

function toUppercase(string) {
  if (typeof string !== "string") {
    return Promise.reject(TypeError("Expected string"));
  }

  const result = string.toUpperCase();

  return Promise.resolve(result);
}

从技术上讲,这段代码中没有任何异步的内容,但它可以很好地说明 Promise 的错误处理机制。

现在我们就可以在 then 中使用结果,并使用 catch 来处理被拒绝的 Promise:

toUppercase(99)
  .then(result => result)
  .catch(error => console.error(error.message));

输出结果如下: 在 Promise 中,catch 是用来处理错误的。除了 catch 还有 finally,类似于 try/catch 中的finally。不管 Promise 结果如何,finally 都会执行:

toUppercase(99)
  .then(result => result)
  .catch(error => console.error(error.message))
  .finally(() => console.log("Finally"));

输出结果如下:

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

(5)Promise, error, throw

作为拒绝 Promise 时的最佳实践,可以传入 error 对象:

Promise.reject(TypeError("Expected string"));

这样,在整个代码库中保持错误处理的一致性。 其他团队成员总是可以访问 error.message,更重要的是可以检查堆栈跟踪。

除了 Promise.reject 之外,还可以通过抛出异常来退出 Promise 执行链。来看下面的例子:

Promise.resolve("A string").then(value => {
  if (typeof value === "string") {
    throw TypeError("Expected number!");
  }
});

这里使用 字符串来 resolve 一个 Promise,然后执行链立即使用 throw 断开。为了停止异常的传播,可以使用 catch 来捕获错误:

Promise.resolve("A string")
  .then(value => {
    if (typeof value === "string") {
      throw TypeError("Expected number!");
    }
  })
  .catch(reason => console.log(reason.message));

这种模式在 fetch 中很常见,可以通过检查 response 对象来查找错误:

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.

所以,最好去捕获错误。

(6)使用 Promise 处理定时器错误

对于计时器或事件,不能捕获回调抛出的异常。上面有一个例子:

function failAfterOneSecond() {
  setTimeout(() => {
    throw Error("Error");
  }, 1000);
}

// 不生效
try {
  failAfterOneSecond();
} catch (error) {
  console.error(error.message);
}

我们可以使用 Promise 来包装计时器:

function failAfterOneSecond() {
  return new Promise((_, reject) => {
    setTimeout(() => {
      reject(Error("Error"));
    }, 1000);
  });
}

这里通过 reject 捕获了一个 Promise 拒绝,它带有一个 error 对象。此时就可以用 catch 来处理异常了:

failAfterOneSecond().catch(reason => console.error(reason.message));

这里使用 value 作为 Promise 的返回值,使用 reason 作为拒绝的返回对象。

(7)Promise.all 的错误处理

Promise.all 方法接受一个 Promise 数组,并返回所有解析 Promise 的结果数组:

const promise1 = Promise.resolve("one");
const promise2 = Promise.resolve("two");

Promise.all([promise1, promise2]).then((results) => console.log(results));

// 结果: ['one', 'two']

如果这些 Promise 中的任何一个被拒绝,Promise.all 将拒绝并返回第一个被拒绝的 Promise 的错误。

为了在 Promise.all 中处理这些情况,可以使用 catch:

const promise1 = Promise.resolve("good");
const promise2 = Promise.reject(Error("Bad"));
const promise3 = Promise.reject(Error("Bad+"));

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("Finally"));

(8)Promise.any 的错误处理

Promise.any 和 Promise.all 恰恰相反。Promise.all 如果某一个失败,就会抛出第一个失败的错误。而 Promise.any 总是返回第一个成功的 Promise,无论是否发生任何拒绝。

相反,如果传递给 Promise.any 的所有 Promise 都被拒绝,那产生的错误就是 AggregateError。 来看下面的例子:

const promise1 = Promise.reject(Error("Error"));
const promise2 = Promise.reject(Error("Error+"));

Promise.any([promise1, promise2])
  .then(result => console.log(result))
  .catch(error => console.error(error))
  .finally(() => console.log("Finally"));

输出结果如下: 这里用 catch 处理错误。AggregateError 对象具有与基本错误相同的属性,外加一个 errors 属性:

const promise1 = Promise.reject(Error("Error"));
const promise2 = Promise.reject(Error("Error+"));

Promise.any([promise1, promise2])
  .then(result => console.log(result))
  .catch(error => console.error(error.errors))
  .finally(() => console.log("Finally"));

此属性是一个包含所有被拒绝的错误信息的数组:

(9)Promise.race 的错误处理

Promise.race 接受一个 Promise 数组,并返回第一个成功的 Promise 的结果:

const promise1 = Promise.resolve("one");
const promise2 = Promise.resolve("two");

Promise.race([promise1, promise2]).then(result => 
  console.log(result)
);

// 结果:one

那如果有被拒绝的 Promise,但它不是传入数组中的第一个呢:

const promise1 = Promise.resolve("one");
const rejection = Promise.reject(Error("Bad"));
const promise2 = Promise.resolve("two");

Promise.race([promise1, rejection, promise2]).then(result =>
  console.log(result)
);

// 结果:one

这样结果还是 one,不会影响正常的执行。

如果被拒绝的 Promise 是数组的第一个元素,则 Promise.race 拒绝,就必须要必须捕获拒绝:

const promise1 = Promise.resolve("one");
const rejection = Promise.reject(Error("Bad"));
const promise2 = Promise.resolve("two");

Promise.race([rejection, promise1, promise2])
  .then(result => console.log(result))
  .catch(error => console.error(error.message));

// Bad

(10)Promise.allSettled 的错误处理

Promise.allSettled 是 ECMAScript 2020 新增的 API。它和 Promise.all 类似,不过不会被短路,也就是说当Promise全部处理完成后,可以拿到每个 Promise 的状态, 而不管其是否处理成功。

来看下面的例子:

const promise1 = Promise.resolve("Good!");
const promise2 = Promise.reject(Error("Bad!"));

Promise.allSettled([promise1, promise2])
  .then(results => console.log(results))
  .catch(error => console.error(error))
  .finally(() => console.log("Finally"));

这里向 Promise.allSettled 传递了一个包含两个 Promise 的数组:一个已解决,另一个已拒绝。

输出结果如下:

(11)async/await 的错误处理

JavaScript 中的 async/await 表示异步函数,用同步的方式去编写异步,可读性更好。

下面来改编上面的同步函数 toUppercase,通过将 async 放在 function 关键字之前将其转换为异步函数:

async function toUppercase(string) {
  if (typeof string !== "string") {
    throw TypeError("Expected string");
  }

  return string.toUpperCase();
}

只需在 function 前加上 async 前缀,就可以让函数返回一个 Promise。这意味着我们可以在函数调用之后链式调用 then、catch 和 finally:

oUppercase("hello")
  .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("Expected string");
  }

  return string.toUpperCase();
}

async function consumer() {
  try {
    await toUppercase(98);
  } catch (error) {
    console.error(error.message);
  } finally {
    console.log("Finally");
  }
}

consumer();

输出结果如下:

(12)异步生成器的错误处理

JavaScript 中的异步生成器是能够生成 Promise 而不是简单值的生成器函数。它将生成器函数与异步相结合,结果是一个生成器函数,其迭代器对象向消费者公开一个 Promise。

要创建一个异步生成器,需要声明一个带有星号 * 的生成器函数,前缀为 async:

async function* asyncGenerator() {
  yield 33;
  yield 99;
  throw Error("Bad!"); // Promise.reject
}

因为异步生成器是基于 Promise,所以同样适用 Promise 的错误处理规则,在异步生成器中,throw 会导致 Promise 拒绝,可以用 catch 拦截它。

要想从异步生成器处理 Promise,可以使用 then:

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));

输出结果如下:

也使用异步迭代 for await...of。 要使用异步迭代,需要用 async 函数包装 consumer:

async function* asyncGenerator() {
  yield 33;
  yield 99;
  throw Error("Bad"); // 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("Bad"); // Promise.reject
}

async function consumer() {
  try {
    for await (const value of asyncGenerator()) {
      console.log(value);
    }
  } catch (error) {
    console.error(error.message);
  }
}

consumer();

输出结果如下: 从异步生成器函数返回的迭代器对象也有一个 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("Reject!"));

go.next().then(value => console.log(value)); 

输出结果如下: 可以通过以下方式来捕获错误:

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("Reject!"));

go.next().then(value => console.log(value));

5 . Node.js 错误处理

(1)同步错误处理

Node.js 中的同步错误处理与 JavaScript 是一样的,可以使用 try/catch/finally。

(2)异步错误处理:回调模式

对于异步代码,Node.js 强烈依赖两个术语:

  • 事件发射器
  • 回调模式

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

来看下面的例子:

const { readFile } = require("fs");

function readDataset(path) {
  readFile(path, { encoding: "utf8" }, function(error, data) {
    if (error) console.error(error);
    // data操作
  });
}

这里可以看到回调中错误处理:

function(error, data) {
    if (error) console.error(error);
    // data操作
}

如果使用 fs.readFile 读取给定路径时出现任何错误,我们都会得到一个 error 对象。这时我们可以:

  • 单地记录错误对象。
  • 抛出异常。
  • 将错误传递给另一个回调。

要想抛出异常,可以这样做:

const { readFile } = require("fs");

function readDataset(path) {
  readFile(path, { encoding: "utf8" }, function(error, data) {
    if (error) throw Error(error.message);
    // data操作
  });
}

但是,与 DOM 中的事件和计时器一样,这个异常会使程序崩溃。 使用 try/catch 停止它的尝试将不起作用:

const { readFile } = require("fs");

function readDataset(path) {
  readFile(path, { encoding: "utf8" }, function(error, data) {
    if (error) throw Error(error.message);
    // 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);
    // data操作
  });
}

这里的 errorHandler 是一个简单的错误处理函数:

function errorHandler(error) {
  console.error(error.message);
  // 处理错误:写入日志、发送到外部logger
}

(3)异步错误处理:事件发射器

Node.js 中的大部分工作都是基于事件的。大多数时候,我们会与发射器对象和一些侦听消息的观察者进行交互。

Node.js 中的任何事件驱动模块(例如 net)都扩展了一个名为 EventEmitter 的根类。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;
      ^

Error: listen EACCES: permission denied 127.0.0.1:80
Emitted 'error' event on Server instance at: ...

为了捕获它,可以为 error 注册一个事件处理函数:

server.on("error", function(error) {
  console.error(error.message);
});

这样就会输出:

listen EACCES: permission denied 127.0.0.1:80

6 . 错误处理最佳实践

最后,我们来看看处理 JavaScript 异常的最佳实践!

(1)不要过度处理错误

错处理的第一个最佳实践就是不要过度使用“错误处理”。通常,我们会在外层处理错误,从内层抛出错误,这样一旦出现错误,就可以更好地理解是什么原因导致的。

然而,开发人员常犯的错误之一是过度使用错误处理。有时这样做是为了让代码在不同的文件和方法中看起来保持一致。但是,不幸的是,这些会对应用程序和错误检测造成不利影响。

因此,只关注代码中可能导致错误的地方,错误处理将有助于提高代码健壮性并增加检测到错误的机会。

(2)避免浏览器特定的非标准方法

尽管许多浏览器都遵循一个通用标准,但某些特定于浏览器的 JavaScript 实现在其他浏览器上却失败了。例如,以下语法仅适用于 Firefox:

catch(e) { 
  console.error(e.filename + ': ' + e.lineNumber); 
}

因此,在处理错误时,尽可能使用跨浏览器友好的 JavaScript 代码。

(3)远程错误记录

当发生错误时,我们应该得到通知以了解出了什么问题。这就是错误日志的用武之地。JavaScript 代码是在用户的浏览器中执行的。因此,需要一种机制来跟踪客户端浏览器中的这些错误,并将它们发送到服务器进行分析。

可以尝试使用以下工具来监控并上报错误:

  • Sentry(https://sentry.io/): 专注于异常(应用崩溃)而不是信息错误。它提供了应用中错误的完整概述,包括受影响的用户数量、调用堆栈、受影响的浏览器以及导致错误的提交等详细信息。
  • Rollbar(https://rollbar.com/): 用于前端、后端和移动应用的无代理错误监控工具。它提供人工智能辅助的工作流程,使开发人员能够在错误影响用户之前立即采取行动。它会显示受错误影响的客户数量、受影响的平台或浏览器的类型以及之前是否发生过类似错误或是否已经存在解决方案等数据。

(4)错误处理中间件(Node.js)

Node.js 环境支持使用中间件向服务端应用中添加功能。因此可以创建一个错误处理中间件。使用中间件的最大好处是所有错误都在一个地方集中处理。可以选择启用/禁用此设置以轻松进行测试。

以下是创建基本中间件的方法:

const logError = err => {
    console.log("ERROR: " + String(err))
}

const errorLoggerMiddleware = (err, req, res, next) => {
    logError(err)
    next(err)
}

const returnErrorMiddleware = (err, req, res, next) => {
    res.status(err.statusCode || 500)
       .send(err.message)
}

module.exports = {
    logError,
    errorLoggerMiddleware,
    returnErrorMiddleware
}

可以像下面这样在应用中使用此中间件:

const { errorLoggerMiddleware, returnErrorMiddleware } = require('./errorMiddleware')

app.use(errorLoggerMiddleware)

app.use(returnErrorMiddleware)

现在可以在中间件内定义自定义逻辑以适当地处理错误。而无需再担心在整个代码库中实现单独的错误处理结构。

(5)捕获所有未捕获的异常(Node.js)

我们可能永远无法涵盖应用中可能发生的所有错误。因此,必须实施回退策略以捕获应用中所有未捕获的异常。

可以这样做:

process.on('uncaughtException', error => {
    console.log("ERROR: " + String(error))
    // 其他处理机制
})

还可以确定发生的错误是标准错误还是自定义操作错误。根据结果,可以退出进程并重新启动它以避免意外行为。

(6)捕获所有未处理的 Promise 拒绝(Node.js)

与异常不同的是,promise 拒绝不会抛出错误。因此,一个被拒绝的 promise 可能只是一个警告,这让应用有可能遇到意外行为。因此,实现处理 promise 拒绝的回退机制至关重要。

可以这样做:

const promiseRejectionCallback = error => {
    console.log("PROMISE REJECTED: " + String(error))
}

process.on('unhandledRejection', callback)

参考文章

  • https://www.valentinog.com/blog/error/
  • https://kinsta.com/blog/errors-in-javascript/
  • https://blog.bitsrc.io/javascript-exception-handling-patterns-best-practices-f7d6fcab735d

喜欢点赞,再看,转发谢谢!


本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/jo4hbYbV4WsXbaeftTixcw

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 相关文章
Java 中验证时间格式的 4 种方法 2年以前  |  3810次阅读
Java经典面试题答案解析(1-80题) 4年以前  |  3594次阅读
CentOS 配置java应用开机自动启动 4年以前  |  2761次阅读
IDEA依赖冲突分析神器—Maven Helper 4年以前  |  2747次阅读
SpringBoot 控制并发登录的人数教程 4年以前  |  2424次阅读
 目录