在回调中获取错误源的正确模式是什么?

问题描述

问题

我想出的解决方案通常是如何解决这个问题的?或者是否有更常见、更直接、“更好”的解决方法?这些替代方案是什么?它们的好处是什么?

详情

我正在用 Node.js 编写一个脚本,我希望该脚本fail fast。因此,我编写了以下函数以在多个地方用作回调:

function throwIfError(err) {
  if(err) throw err;
}

但是,当我使用它时,比如 fs.writeFile,我没有得到有用的堆栈跟踪:

function throwIfError(err) {
  if(err) throw err;
}

require('fs').writeFile(
  'some_non_existent_directory/some_file','',throwIfError,);
$ node script.js
/path/to/script.js:2
  if(err) throw err;
          ^

[Error: ENOENT: no such file or directory,open 'some_non_existent_directory/some_file'] {
  errno: -2,code: 'ENOENT',syscall: 'open',path: 'some_non_existent_directory/some_file'
}
$

这里没有任何内容告诉我错误的来源是第 5 行对 writeFile调用。脚本可能有多个 writeFile 调用,我不知道错误源自何处。

使用 console.trace() 也无济于事:

function throwIfError(err) {
  console.trace();
}

require('fs').writeFile(
  'some_non_existent_directory/some_file',);
$ node script.js
Trace
    at throwIfError (/path/to/script.js:2:11)
    at fs.js:1448:7
    at FSReqCallback.oncomplete (fs.js:171:23)
$

不是在一个地方定义回调函数并在多个地方使用它,而是在每个函数调用中定义内联回调来修复它:

require('fs').writeFile(
  'some_non_existent_directory/some_file',err => {if(err) throw err},);
$ node script.js
/path/to/script.js:4
  err => {if(err) throw err},^

[Error: ENOENT: no such file or directory,path: 'some_non_existent_directory/some_file'
}
$

由于回调现在是内联的,所以抛出错误的地方与进行“有问题”函数调用的地方是同一个地方。由于运行时会告诉我抛出错误的位置,这意味着我隐式地知道错误发生在哪个函数调用中。

然而,问题在于代码重复。这样做意味着在许多地方一遍又一遍地定义相同的函数(回调)。如果我想更改回调,所有都需要更改,因为所有实际上都是彼此重复的。另一个问题是垃圾收集器的负载增加。在前一种方式中,只有一个回调并且只是对它的引用。但是,这样一来,回调的次数和使用的次数一样多。

一个问题(重复的回调定义以及当你需要改变一个时需要改变每一个)可以用下面的“辅助回调”模式来解决

require('fs').writeFile(
  'some_non_existent_directory/some_file',err => throwIfError(err),);

function throwIfError(err) {
  if(err) {
    console.log('Callback is located at:');
    console.trace();
    throw err;
  }
}
$ node script.js
Callback is located at:
Trace
    at throwIfError (/path/to/the/script.js:10:13)
    at /path/to/the/script.js:4:10
    at fs.js:1448:7
    at FSReqCallback.oncomplete (fs.js:171:23)
/path/to/the/script.js:11
    throw err;
    ^

[Error: ENOENT: no such file or directory,path: 'some_non_existent_directory/some_file'
}
$

在这里console.trace 输出中的第二行让我了解回调的使用位置。这样,我就知道是哪个函数调用导致了这个错误

这种模式的一个优点是,如果(真实的)回调需要改变,它可以简单地在一个地方改变,也就是它被定义的地方。但是,仍然存在垃圾收集器负载增加的问题,因为我们在每个需要使用 throwIfError 回调的地方都定义了匿名“助手”回调。

这个想法的一个更高级的实现如下:

require('fs').writeFile(
  'some_non_existent_directory/some_file',);

// NOTE: This works only if it is used with an inline helper callback like `err
// => throwIfError(err)`,which is a function _expression_ that's passed to a
// function invocation which expects a callback. If the following function is
// used by itself,like `throwIfError`,it won't work!
function throwIfError(err) {
  if (!err) return;

  const currentStack = getCurrentStack(
    1,'Info: First line below shows where this callback was used'
  );
  err.stack = currentStack + '\n' + err.stack;
  // Following is so that the stack will be formatted properly when it is
  // printed. The `formatError` function in 'lib/internal/util/inspect.js'
  // ignores everything in the `stack` property until the end of the value of
  // the `message` property in the `stack` property. That is,the `err.message`
  // is present in `err.stack` by default,because when creating an error with
  // an argument,the argument becomes the `message` property,and the value of
  // the `stack` property is generated using the:
  //
  // - Name of the error type
  // - Error message
  // - Stack trace at the time that the error is created
  //
  // Since the `message` property is present in the `stack` property by default,// and since the `formatError` function in 'lib/internal/util/inspect.js'
  // ignores everything until the end of the value of the `message` in the
  // `stack` property,we remove the `message` property so that the part of the
  // stack before the value of the `message` property will not be ignored (that
  // is,it will be formatted properly for printing).
  //
  // The lines in the `formatError` function in 'lib/internal/util/inspect.js'
  // that cause this are as follows:
  //
  //     // Ignore the error message if it's contained in the stack.
  //     let pos = (err.message && stack.indexOf(err.message)) || -1;
  //     if (pos !== -1)
  //       pos += err.message.length;
  delete err.message;

  throw err;
}

function getCurrentStack(
  numAdditionalFramesToRemove = 0,message = 'Info: Current stack is'
) {
  const stackContainer = {};
  Error.captureStackTrace(stackContainer);

  stackContainer.stack = stackContainer.stack.split('\n');
  // The magic number 2 below means "Always remove two stack frames". The reason
  // is:
  //
  // - The first line of the output of `Error.captureStackTrace` includes a line
  //   that just says "Error". This is not useful and we are replacing it with
  //   our custom message using the `message` parameter already.
  // - The second line is the frame for the invocation of this method. It is
  //   Meta information to the user and hence,we are removing it.
  stackContainer.stack.splice(0,2 + numAdditionalFramesToRemove,message);
  stackContainer.stack = stackContainer.stack.join('\n');

  return stackContainer.stack;
}
$ node script.js
/path/to/the/script.js:47
  throw err;
  ^

Info: First line below shows where this callback was used
    at /path/to/the/script.js:4:10
    at fs.js:1448:7
    at FSReqCallback.oncomplete (fs.js:171:23)
Error: ENOENT: no such file or directory,open 'some_non_existent_directory/some_file' {
  errno: -2,path: 'some_non_existent_directory/some_file'
}
$

虽然这并不能解决由于匿名“helper”回调函数导致垃圾收集器负载增加的问题,但这是我能想到的最好的方法

解决方法

暂无找到可以解决该程序问题的有效方法,小编努力寻找整理中!

如果你已经找到好的解决方法,欢迎将解决方案带上本链接一起发送给小编。

小编邮箱:dio#foxmail.com (将#修改为@)