Node.js中的错误处理

509 阅读7分钟

错误是开发者生活的一部分。我们既不能逃避也不能躲避它们。在构建生产就绪的软件时,我们需要有效地管理错误,以。

  1. 改善终端用户的体验;即提供正确的信息,而不是 "无法满足请求 "的一般信息
  2. 开发一个强大的代码库
  3. 通过有效地发现错误来减少开发时间
  4. 避免突然停止一个程序

因为你在这里,我假设你可能是一个具有JavaScript背景的网络开发人员。让我们以Node.js中读取文件而不处理错误的典型用例为例。

var fs = require('fs')

# read a file
const data = fs.readFileSync('/Users/Kedar/node.txt')

console.log("an important piece of code that should be run at the end")

请注意,Node.js应该在读取文件的任务之后执行一些关键的代码。当我们运行它时,我们会收到如下所示的输出。

输出。

$node main.js
fs.js:641
  return binding.open(pathModule._makeLong(path), stringToFlags(flags), mode);
                 ^

Error: ENOENT: no such file or directory, open '/Users/Kedar/node.txt'
    at Error (native)
    at Object.fs.openSync (fs.js:641:18)
    at Object.fs.readFileSync (fs.js:509:33)
    at Object.<anonymous> (/home/cg/root/7717036/main.js:3:17)
    at Module._compile (module.js:570:32)
    at Object.Module._extensions..js (module.js:579:10)
    at Module.load (module.js:487:32)
    at tryModuleLoad (module.js:446:12)
    at Function.Module._load (module.js:438:3)
    at Module.runMain (module.js:604:10)

这里,程序没有执行必要的代码就突然结束了。我们将在后面的try...catch 块部分讨论修改后的带错误处理的代码。这个例子只是展示了没有错误处理所面临的许多问题中的一个。让我们来看看我们将涉及的内容,以便更好地理解如何处理错误。

在我们学习错误处理之前,让我们了解一下Node.js中的Error

错误

Error 是对 [Error](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error)对象在Javascript中的延伸。错误可以被构造并抛出 ,或者传递给一些函数。让我们来看看一些例子。

throw new Error('bad request'); // throwing new error
callback_function(new Error('connectivity issue')); // passing error as an argument

在创建错误时,我们需要传递一个人类可读的字符串作为参数,以了解当我们的程序工作不正常时出了什么问题。换句话说,我们是通过向Error 构造函数传递字符串来创建一个对象。

你还需要知道,错误和异常在JavaScript中是不同的,特别是Node.js。错误是Error 类的实例,当你抛出一个错误时,它就成为一个异常。

人类不会造成所有的错误。有两种类型的错误,程序员和操作人员。我们用 "错误 "这个短语来描述这两种错误,但由于它们的根本原因,它们在现实中是完全不同的。让我们来看看每一种。

程序员错误

程序员错误描述的是所写程序的问题--bug。换句话说,这些是由程序员在编写程序时的错误造成的错误。我们无法正确处理这些错误,纠正这些错误的唯一方法是修复代码库。下面是一些常见的程序员错误。

  • 数组索引出界--在只有六个元素的情况下,试图访问数组的第七个元素
  • 语法错误--在定义一个JavaScript函数时没有关闭大括号
  • 引用错误--访问一个没有定义的函数或变量
  • 弃用错误和警告--调用没有回调的异步函数
  • 类型错误--x 对象不是可迭代的
  • 未能处理操作错误

操作性错误

每个程序都会面临操作错误(即使程序是正确的)。这些都是运行期间由于外部因素造成的问题,会打断程序的正常流程。与程序员错误不同,我们可以理解并处理它们。这些是一些例子。

  • 无法连接服务器/数据库
  • 请求超时
  • 用户的无效输入
  • 套接字挂起
  • 来自服务器的500响应
  • 找不到文件

你可能想知道为什么这种隔离是必要的,因为两者都会导致同样的效果,即中断程序?好吧,你可能要根据错误的类型来采取行动。例如,重启应用程序可能不是一个合适的行动,因为文件未找到错误(操作错误),但当你的程序未能捕捉到被拒绝的承诺时,重启可能会有帮助(程序员错误)。

现在你知道了这些错误,让我们来处理它们。我们可以通过管理这些错误来避免程序的突然终止,这是生产就绪的代码的一个重要部分。

错误处理技术

为了有效地处理错误,我们需要了解错误传递技术。

在Node.js中,有四种基本策略来报告错误。

  1. try…catch
  2. 回调
  3. 许诺
  4. 事件发射器

让我们来逐一了解使用它们。

try…catch

[try…catch](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/try...catch)方法中,try 块包围了可能发生错误的代码。换句话说,我们包裹了我们想要检查错误的代码;catch 块在这个块中处理异常。

下面是处理错误的try…catch 代码。

var fs = require('fs')

try {
const data = fs.readFileSync('/Users/Kedar/node.txt')
} catch (err) {
  console.log(err)
}

console.log("an important piece of code that should be run at the end")

我们收到如下所示的输出。

$node main.js
{ Error: ENOENT: no such file or directory, open '/Users/Kedar/node.txt'
    at Error (native)
    at Object.fs.openSync (fs.js:641:18)
    at Object.fs.readFileSync (fs.js:509:33)
    at Object.<anonymous> (/home/cg/root/7717036/main.js:4:17)
    at Module._compile (module.js:570:32)
    at Object.Module._extensions..js (module.js:579:10)
    at Module.load (module.js:487:32)
    at tryModuleLoad (module.js:446:12)
    at Function.Module._load (module.js:438:3)
    at Module.runMain (module.js:604:10)
  errno: -2,
  code: 'ENOENT',
  syscall: 'open',
  path: '/Users/Kedar/node.txt' }
an important piece of code that should be run at the end

该错误被处理并显示。最后,代码的其他部分按计划执行。

回调

回调函数(通常用于异步代码)是我们实现错误处理的函数的一个参数。

回调函数的目的是在主函数的结果被使用之前检查错误。回调通常是主函数的最后一个参数,当出现错误或操作的结果时,它就会执行。

下面是回调函数的语法。

function (err, result) {}

第一个参数用于错误,第二个参数用于结果。如果出现错误,第一个属性将包含错误,而第二个属性将是未定义的,反之亦然。让我们看看一个例子,我们试图通过应用这种技术来读取一个文件。

const fs = require('fs');

fs.readFile('/home/Kedar/node.txt', (err, result) => {
  if (err) {
    console.error(err);
    return;
  }

  console.log(result);
});

结果看起来是这样的。

$node main.js
{ Error: ENOENT: no such file or directory, open '/home/Kedar/node.txt'
    at Error (native)
  errno: -2,
  code: 'ENOENT',
  syscall: 'open',
  path: '/home/Kedar/node.txt' }

我们收到了错误,因为该文件不可用。我们也可以用用户定义的函数实现回调。下面的例子说明了一个用户定义的函数,使用回调将给定的数字加倍。

const udf_double = (num, callback) => {
  if (typeof callback !== 'function') {
    throw new TypeError(`Expected the function. Got: ${typeof callback}`);
  }

  // simulate the async operation
  setTimeout(() => {
    if (typeof num !== 'number') {
      callback(new TypeError(`Expected number, got: ${typeof num}`));
      return;
    }

    const result = num * 2;
    // callback invoked after the operation completes.
    callback(null, result);
  }, 100);
}

// function call
udf_double('2', (err, result) => {
  if (err) {
    console.error(err)
    return
  }
  console.log(result);
});

上面的程序会出现错误,因为我们传递的是字符串而不是整数。结果如下。

$node main.js
TypeError: Expected number, got: string
    at Timeout.setTimeout (/home/cg/root/7717036/main.js:9:16)
    at ontimeout (timers.js:386:14)
    at tryOnTimeout (timers.js:250:5)
    at Timer.listOnTimeout (timers.js:214:5)

承诺

[Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)在Node.js中,承诺是一种处理错误的现代方式,与回调相比,它通常是首选。由于承诺是回调的替代品,让我们把上面讨论的例子 (udf_double) 转换为利用承诺。

const udf_double = num => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (typeof num !== 'number') {
        reject(new TypeError(`Expected number, got: ${typeof num}`));
      }

      const result = num * 2;
      resolve(result);
    }, 100);
  });
}

在函数中,我们将返回一个承诺,这是我们主要逻辑的一个包装。我们在定义Promise 对象时传递了两个参数。

  1. resolve - 用于解决承诺并提供结果
  2. reject - 用于报告/抛出错误

现在,让我们通过传递输入来执行这个函数。

udf_double('8')
  .then((result) => console.log(result))
  .catch((err) => console.error(err));

我们得到一个错误,如下图所示。

$node main.js
TypeError: Expected number, got: string
    at Timeout.setTimeout (/home/cg/root/7717036/main.js:5:16)
    at ontimeout (timers.js:386:14)
    at tryOnTimeout (timers.js:250:5)
    at Timer.listOnTimeout (timers.js:214:5)

好了,这看起来比回调简单多了。我们也可以使用诸如util.promisify() 这样的工具,将基于回调的代码转换为Promise 。让我们把回调部分的fs.readFile 例子转换为使用promisify

const fs = require('fs');
const util = require('util');

const readFile = util.promisify(fs.readFile);

readFile('/home/Kedar/node.txt')
  .then((result) => console.log(result))
  .catch((err) => console.error(err));

在这里,我们对readFile 函数进行承诺。我们得到的结果如下。

[Error: ENOENT: no such file or directory, open '/home/Kedar/node.txt'] {
  errno: -2,
  code: 'ENOENT',
  syscall: 'open',
  path: '/home/Kedar/node.txt'
}

Async/await

Async/await只是语法上的糖,目的是为了增强承诺。它为异步代码提供了一个同步的结构。对于简单的查询,Promise的 可以很容易地使用。

不过,如果你遇到复杂的查询场景,看起来好像是同步的代码会更容易理解。

请注意,async 函数的返回值是一个Promiseawait 等待该承诺被解决或拒绝。让我们使用async/await来实现readFile 的例子。

const fs = require('fs');
const util = require('util');

const readFile = util.promisify(fs.readFile);

const read = async () => {
  try {
    const result = await readFile('/home/Kedar/node.txt');
    console.log(result);
  } catch (err) {
    console.error(err);
  }
};

read()

我们正在创建async read函数,其中我们使用await 读取文件。其输出结果如下。

[Error: ENOENT: no such file or directory, open '/home/Kedar/node.txt'] {
  errno: -2,
  code: 'ENOENT',
  syscall: 'open',
  path: '/home/Kedar/node.txt'
}

事件发射器

我们可以使用事件模块中的EventEmitter类来报告复杂场景中的错误--冗长的异步操作会产生许多故障。我们可以连续发射所造成的错误,并使用发射器来监听它们。

让我们看看这个例子,我们试图模仿一个接收数据的场景,并检查它是否正确。我们需要检查前六个索引是否是整数,不包括第十二个索引。如果这六个索引中的任何一个不是整数,我们就会发出一个错误,根据这个错误做进一步的决定。

const { EventEmitter } = require('events'); //importing module

const getLetter = (index) =>{
    let cypher = "*12345K%^*^&*" //will be a fetch function in a real scenario which will fetch a new cypher every time
    let cipher_split = cipher.split('')
    return cipher_split[index]
}

const emitterFn = () => {
  const emitter = new EventEmitter(); //initializing new emitter
  let counter = 0;
  const interval = setInterval(() => {
    counter++;
    
    if (counter === 7) {
      clearInterval(interval);
      emitter.emit('end');
    }
    
    let letter = getLetter(counter)
    
    if (isNaN(letter)) { //Check if the received value is a number
      (counter<7) && emitter.emit(
        'error',
        new Error(`The index ${counter} needs to be a digit`)
      );
      return;
    }
    (counter<7) && emitter.emit('success', counter);

  }, 1000);

  return emitter;
}

const listner = emitterFn();

listner.on('end', () => {
  console.info('All six indexes have been checked');
});

listner.on('success', (counter) => {
  console.log(`${counter} index is an integer`);
});

listner.on('error', (err) => {
  console.error(err.message);
});

首先,我们导入events 模块,使用EventEmitter 。然后,我们定义getLetter() 函数,以获取新的密码,并在每次由emitterFn() 要求的特定索引上发送值。emitterFn() 将启动EventEmitter 对象。我们逐一获取所有六个索引上的值,如果它不是一个整数,就会发出一个错误。

一个变量存储了从emitterFn() 收到的值,我们用listener.on() 来监听它们。检查完所有的索引后,程序将结束。输出看起来如下所示。

1 index is an integer
2 index is an integer
3 index is an integer
4 index is an integer
5 index is an integer
The index 6 needs to be a digit
All six indexes have been checked

处理错误

现在你知道了报告错误的技术,让我们来处理它们。

重试操作

有时,外部系统对有效的请求可能会造成错误。例如,在使用API获取一些坐标时,你得到503错误,服务不可用,这是由于过载或网络故障造成的。

这时,服务可能在几秒钟后就会恢复,报告错误可能不是最理想的做法,所以你重试操作。另外,如果你在堆栈的深处,这可能不是一个好主意,因为所有层都在不断重试,等待时间严重延长。在这种情况下,最好是中止,让客户端从他们那边重试。

向客户端报告失败

当从客户端收到错误的输入时,重试是没有意义的,因为我们可能在重新处理错误的信息时得到同样的结果。在这种情况下,最直接的方法是完成剩下的处理并向客户端报告。

直接报告堆栈顶部的故障

有时直接报告错误是合适的,因为你可能知道原因。例如,在try…catch blocks 部分讨论的ENOENT 错误是在你试图打开一个不存在的文件时产生的,你可以使用上面讨论的任何方法来报告它。通过这种方式,你可以报告创建文件来解决这个错误。

立即崩溃

在出现无法恢复的错误时,崩溃程序是最好的选择。例如,一个错误是由于在没有权限的情况下访问一个文件造成的;除了崩溃系统并让系统管理员提供访问权之外,你没有什么可以做。崩溃也是处理程序员错误以恢复系统的最实用的方法。

结论

总而言之,如果你努力写好代码并提供可靠的软件,适当的错误处理是必须的。在这篇文章中,我们了解了Node.js中的错误和处理错误的重要性。我们讨论了报告错误的初步方法:try…catch 块、回调和承诺、async/await 语法以及事件发射器。我们还学习了一旦报告这些错误后的处理方法。

最后,我希望这对你有帮助,现在你可以在你的Node.js应用程序中处理错误了。

The postError handling in Node.jsappeared first onLogRocket Blog.