# 错误处理技巧

返回:js常见方法

错误集锦及不良习惯

什么是编程中的错误

我们的开发过程中并不总是一帆风顺。特别是在某些情况下,我们可能希望停止程序或在发生不良情况时通知用户。

# JavaScript 中的错误是什么

返回顶部

JavaScript中的错误是一个对象。要在 JS 创建一个错误,可以使用 Error 对象,如下所示:

const err = new Error('霍霍,好像哪里出问题了!');

const err = Error('霍霍,好像哪里出问题了!')
1
2
3

# 错误对象有三个属性

返回顶部

  • message:带有错误消息的字符串
  • name:错误的类型
  • stack:函数执行的堆栈跟踪
const wrongType = TypeError("霍霍,好像哪里出问题了!")
wrongType.message // "霍霍,好像哪里出问题了!"
wrongType.name // "TypeError"
1
2
3

# JavaScript中的许多类型的错误

返回顶部

错误类型

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

所有这些错误类型都是实际的构造函数,意味着返回一个新的错误对象。

TIP

在我们的代码中,主要还是使用ErrorTypeError这两种最常见的类型来创建自己的错误对象
大多数时候,大多数错误将直接来自JavaScript引擎,例如InternalError或SyntaxError。

# 什么是异常

返回顶部

什么是异常

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

要在JavaScript中引发异常,我们使用throw 关键字把错误抛出去:

const wrongType = TypeError("霍霍,好像哪里出问题了!")
throw wrongType;
1
2

简写形式:

throw TypeError("霍霍,好像哪里出问题了!")
1

或者

throw new TypeError("霍霍,好像哪里出问题了!")
1

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

返回顶部

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

WARNING

何时何地捕获代码中的异常取决于特定的用例

# 同步中的错误处理

返回顶部

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

# 常规函数的错误处理

try {
  toUppercase(4);
} catch (error) {
  console.error(error.message);
} finally {
}
1
2
3
4
5
6

try/catch/finally是一个同步结构,但它也可以捕获异步出现的异常。

# 使用 generator 函数来处理错误

返回顶部

JavaScript中的生成器函数是一种特殊的函数。除了在其内部作用域和使用者之间提供双向通信通道之外,还可以随意暂停和恢复。

要创建一个生成器函数,我们在function关键字后面放一个*:

function* generate() {
  //
}
1
2
3

在函数内可以使用yield返回值:

function* generate() {
  yield 33;
  yield 99;
}
1
2
3
4

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

  • 使用 next() 方法
  • 通过 for...of 遍历
function* generate() {
  yield 33;
  yield 99;
}
const go = generate();
const firstStep = go.next().value; // 33
const secondStep = go.next().value; // 99
1
2
3
4
5
6
7

除了next()之外,从生成器返回的迭代器对象还具有throw()方法。使用这种方法,我们可以通过向生成器中注入一个异常来停止程序

function* generate() {
  yield 33;
  yield 99;
}
const go = generate();
const firstStep = go.next().value; // 33
go.throw(Error("我要结束你!"));
const secondStep = go.next().value; // 这里会抛出异常
1
2
3
4
5
6
7
8

要获取此错误,可以在生成器函数中使用 try/catch/finally:

function* generate() {
  try {
    yield 33;
    yield 99;
  } catch (error) {
    console.error(error.message);
  }}
1
2
3
4
5
6
7

下面这个事例是使用 for...of 来获取 生成器函数中的值:

function* generate() {
    yield 33;
    yield 99;
    throw Error("我要结束你!")
}

try {
  for (const value of generate()) {
    console.log(value)
  }} catch (error) {
  console.log(error.message)
}/* 输出:
  33
  99
  我要结束你!
*/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 异步中的错误处理

返回顶部

单线程语言

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

浏览器中异步操作有:

# 定时器的错误处理

返回顶部

function failAfterOneSecond() {
  setTimeout(() => {
    throw Error("Something went wrong!");
  }, 1000);
}
1
2
3
4
5

这个函数大约在1秒后抛出异常,处理这个异常的正确方法是什么?

下面的方法不起作用

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

try {
  failAfterOneSecond();
  }
catch (error) {
  console.error(error.message);
}
1
2
3
4
5
6
7
8
9
10
11
12

我们知道 try/catch 是同步,而 setTimeout 是异步的。当执行到 setTimeout回调时,try/catch 早已跑完了,所以异常就无法捕获到。
它们在两务不同的轨道上:

Track A: --> try/catch
Track B: --> setTimeout --> callback --> throw
1
2

如果能让程序跑下去,把 try/catch 移动到 setTimeout 里面。但这种做法意义不大,后面我们会使用 Promise 来解决这类的问题。

# 事件中错误处理

返回顶部

事件

DOM 的事件操作(监听和触发),都定义在EventTarget接口。
Element节点、document节点和window对象,都部署了这个接口。
此外,XMLHttpRequest、AudioNode、AudioContext等浏览器内置对象,也部署了这个接口。

该接口就是三个方法

  • addEventListener和removeEventListener用于绑定和移除监听函数
  • dispatchEvent用于触发事件。
const button = document.querySelector("button");
button.addEventListener("click", function() {
  throw Error("Can't touch this button!");
});
1
2
3
4

在这里,单击按钮后立即引发异常。我们如何抓住它?下面这种方式没啥作用,也不会阻止程序崩溃:

const button = document.querySelector("button");
try {
  button.addEventListener("click", function() {
    throw Error("Can't touch this button!");
  });} catch (error) {
  console.error(error.message);
}
1
2
3
4
5
6
7

如果能让程序跑下去,把 try/catch 移动到 addEventListener 里面。但这种做法意义不大,后面我们会使用 Promise 来解决这类的问题。

# onerror 怎么样

返回顶部

HTML元素具有许多事件处理程序,例如onclick,onmouseenter,onchange等,当然还有 onerror。
当 img 标签或 script 标签遇到不存在的资源时,onerror事件处理程序都会触发。当文件不存在时,控制台就会报如下的错误:

GET http://localhost:5000/nowhere-to-be-found.png
[HTTP/1.1 404 Not Found 3ms]
1
2

在 JS 中,我们可以通过 onerror 来捕获这个错误:

const image = document.querySelector("img");
image.onerror = function(event) {
  console.log(event);
};
1
2
3
4

更好的方式:

const image = document.querySelector("img");
image.addEventListener("error", function(event) {
  console.log(event);
});
1
2
3
4

这种方式对于一些请求资源丢失的情况很有用,但 onerror 与 throw 与 try/cathc 无关。

# 使用 Promise 处理错误

返回顶部

为了演示 Promise 处理方式,我们先回到一开始的那个事例:

function toUppercase(string) {
  if (typeof string !== "string") {
    throw TypeError("Wrong type given, expected a string");
  }

  return string.toUpperCase();
}

toUppercase(4);
1
2
3
4
5
6
7
8
9

相对简单抛出异常,我们可以使用 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);
}
1
2
3
4
5
6
7
8
9

因为使用了 Promise ,所以可以使用 then 来接收返回的内容,或者用 catch 来捕获出现的错误。

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

除了 then 和 catch , Promise 中还有 finally 方法,这类似于try/catch 中的 finally。

toUppercase(99)
  .then(result => result)
  .catch(error => console.error(error.message))
  .finally(() => console.log("Run baby, run"));
1
2
3
4

# Promise, error, 和 throw

返回顶部

使用 Promise.reject 可以很方便的抛出错误:

Promise.reject(TypeError("Wrong type given, expected a string"));
1

除了Promise.reject,我们也可以通过抛出异常来退出 Promise。

考虑以下示例:

Promise.resolve("A string").then(value => {
  if (typeof value === "string") {
    throw TypeError("Expected a number!");
  }
});
1
2
3
4
5

要停止异常传播,我们照常使用catch:

Promise.resolve("A string")
  .then(value => {
    if (typeof value === "string") {
      throw TypeError("Expected a number!");
    }
  })
  .catch(reason => console.log(reason.message));
1
2
3
4
5
6
7

# 使用 Promise 来处理定时器中的异常

返回顶部

解决方案就是使用 Promise:

function failAfterOneSecond() {
  return new Promise((_, reject) => {
    setTimeout(() => {
      reject(Error("Something went wrong!"));
    }, 1000);
  });
}
1
2
3
4
5
6
7

使用reject,我们启动了一个 Promise 拒绝,它携带一个错误对象。此时,我们可以使用catch处理异常:

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

# 使用 Promise.all 来处理错误

返回顶部

Promise.all(iterable) 方法返回一个 Promise 实例,此实例在 iterable 参数内所有的 promise 都“完成(resolved)”或参数中不包含 promise 时回调完成(resolve);

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!' ]
1
2
3
4
5
6

如果参数中 promise 有一个失败(rejected),此实例回调失败(reject),失败的原因是第一个失败 promise 的结果。

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

// No good, sorry!
1
2
3
4
5
6
7
8
9

同样,无论Promise.all的结果如何运行函数,finally 都会被执行:

Promise.all([promise1, promise2, promise3])
  .then(results => console.log(results))
  .catch(error => console.error(error.message))
  .finally(() => console.log("Always runs!"));
1
2
3
4

# 使用 Promise.any 来处理错误

返回顶部

TIP

Promise.any() (Firefox > 79, Chrome > 85) 接收一个 Promise 可迭代对象,只要其中的一个 promise 成功,就返回那个已经成功的 promise 。如果可迭代对象中没有一个 promise 成功(即所有的 promises 都失败/拒绝),就返回一个失败的 promise 和AggregateError类型的实例,它是 Error 的一个子类,用于把单一的错误集合在一起。本质上,这个方法和Promise.all()是相反的。

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!"));
1
2
3
4
5
6
7

在这里,我们使用catch处理错误,输出如下:

AggregateError: No Promise in Promise.any was resolved
Always runs!
1
2

AggregateError对象具有与基本Error相同的属性,外加errors属性:

//
  .catch(error => console.error(error.errors))
//
1
2
3

此属性是由reject产生的每个单独错误的数组

[Error: "No good, sorry!, Error: "Bad day ..."]
1

# 使用 Promise.race 来处理错误

返回顶部

Promise.race(iterable) 方法返回一个 promise,一旦迭代器中的某个promise解决或拒绝,返回的 promise就会解决或拒绝。

const promise1 = Promise.resolve("The first!");
const promise2 = Promise.resolve("The second!");

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

// The first!
1
2
3
4
5
6

这里说明,第一个 Promise 比第二个行执行完。那包含拒绝的情况又是怎么样的?

const promise1 = Promise.resolve("The first!");
const rejection = Promise.reject(Error("Ouch!"));
const promise2 = Promise.resolve("The second!");

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

// The first!
1
2
3
4
5
6
7
8
9

如果把reject放在第一个又会怎么样?

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!
1
2
3
4
5
6
7
8
9

# 使用 Promise.allSettled 来处理错误

返回顶部

Promise.allSettled()方法返回一个在所有给定的promise都已经fulfilled或rejected后的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!"));
1
2
3
4
5
6
7

我们传递给Promise.allSettled一个由两个Promise组成的数组:一个已解决,另一个被拒绝。

这种情况 catch 不会被执行, finally 永远会执行。

[
  { status: 'fulfilled', value: 'Good!' },
  {
    status: 'rejected',
    reason: Error: No good, sorry!
  }
]
1
2
3
4
5
6
7

# 使用 async/await 来处理错误

返回顶部

async function toUppercase(string) {
  if (typeof string !== "string") {
    throw TypeError("Wrong type given, expected a string");
  }

  return string.toUpperCase();
}
1
2
3
4
5
6
7

只要在函数前面加上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!"));
1
2
3
4
5
6
7
8
9
10
11
12

当从 async 函数抛出异常时,我们就可以使用 catch 来捕获。

最重要的是,除了这种方式外,我们可以还使用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();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

输出:

Wrong type given, expected a string
Always runs!
1
2

# 使用 async generators 来处理错误

返回顶部

JavaScript中的async generators是能够生成 Promises 而不是简单值的生成器函数。

async function* asyncGenerator() {
  yield 33;
  yield 99;
  throw Error("Something went wrong!"); // Promise.reject
}
1
2
3
4
5

基于 Promise,此处适用于错误处理的相同规则。在异步生成器中 throw 将会触发 Promise 的reject,我们可以使用catch对其进行拦截。

为了使用异步生成器的 Promise,我们可以这样做:

  • then 方法
  • 异步遍历

从上面我们知道,在两次调用 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));
1
2
3
4
5

输出结果:

{ value: 33, done: false }
{ value: 99, done: false }
Something went wrong!
1
2
3

别一种是使用 异步遍历与for await...of:

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();
1
2
3
4
5
6
7
8
9
10
11
12
13

有了 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();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

输出结果:

33
99
Something went wrong!
1
2
3

从异步生成器函数返回的迭代器对象也具有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
1
2
3
4
5
6
7
8
9
10
11
12
13
14

要从外部处理这种情况,我们可以做:

go.throw(Error("Let's reject!")).catch(reason => console.error(reason.message));
1

# Node 中的错误处理

返回顶部

# Node 中的同步错误处理

Node.js 中的同步错误处理与到目前为止所看到的并没有太大差异。对于同步,使用 try/catch/finally 就可以很好的工作了。

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

对于异步代码,Node.js 主要使用这两种方式:

  • 回调模式
  • event emitters

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

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

function readDataset(path) {
  readFile(path, { encoding: "utf8" }, function(error, data) {
    if (error) console.error(error);
    // do stuff with the data
  });
}
1
2
3
4
5
6
7
8

我们可以看到,这里处理错误的方式是使用了回调:

function(error, data) {
    if (error) console.error(error);
    // do stuff with the data
  }
//
1
2
3
4
5

我们可以抛出一个异常

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
  });
}
1
2
3
4
5
6
7
8

但是,与 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);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

如果我们不想使程序崩溃,则将错误传递给另一个回调是首选方法:

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

function readDataset(path) {
  readFile(path, { encoding: "utf8" }, function(error, data) {
    if (error) return errorHandler(error);
    // do stuff with the data
  });
}
1
2
3
4
5
6
7
8

这里的errorHandler顾名思义,是一个用于错误处理的简单函数:

function errorHandler(error) {
  console.error(error.message);
  // do something with the error:
  // - write to a log.
  // - send to an external logger.
}
1
2
3
4
5
6

# Node.js 中的异步错误处理:event emitters

在 Node.js 中所做的大部分工作都是基于事件的。大多数情况下,emitter object 和一些观察者进行交互以侦听消息。

Node.js中的任何事件驱动模块(例如net)都扩展了一个名为EventEmitter的根类。

Node.js中的EventEmitter有两种基本方法:onemit

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!");
});
1
2
3
4
5
6
7
8
9
10
11
12

这里我们来听两个事件:listeningconnection。除了这些事件之外,event emitters 还公开一个 error 事件,以防发生错误。

如果在端口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!");
});
1
2
3
4
5
6
7
8
9
10
11
12
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: ...
1
2
3
4
5
6

要捕获它,我们可以注册一个error事件处理程序:

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

输出结果:

listen EACCES: permission denied 127.0.0.1:80
1