【问题标题】:Re-throwing exception in NodeJS and not losing stack trace在 NodeJS 中重新抛出异常并且不丢失堆栈跟踪
【发布时间】:2017-08-02 21:59:45
【问题描述】:

如何在 nodejs/javascript 中重新抛出错误或异常并包含自定义消息。

我有以下代码

var json = JSON.parse(result);

如果发生任何解析错误,我想在异常消息中包含result 内容。像这样。

1.  try {
2.    var json = JSON.parse(result);
3.    expect(json.messages.length).to.be(1);
4.  } catch(ex) {
5.    throw new Error(ex.message + ". " + "JSON response: " + result);
6.  }

这里的问题是我丢失了堆栈跟踪。

有没有类似java的方法?

throw new Error("JSON response: " + result, ex);

【问题讨论】:

  • 不是一个直接的答案,但你真的很想研究这个:joyent.com/node-js/production/design/errors
  • @Paul 虽然是一个很棒的介绍,但我认为现在有点过时了,因为它没有提到解决许多 JS/Node.js 错误处理的变幻莫测的承诺和错误类。跨度>

标签: javascript node.js exception-handling expect.js


【解决方案1】:

我不知道像 Java 这样的本地方法,而且我还没有找到一个优雅的解决方案来包装错误。

创建new Error 的问题是您可能会丢失附加到抛出的原始Error 的元数据,堆栈跟踪和类型通常是丢失的重要项目。

对现有抛出的错误进行修改会更快,但仍然可以从不存在的错误中修改数据。在其他地方创建的错误中四处寻找也感觉不对。

创建一个新的错误和新的堆栈

新的Error.stack 属性是一个纯字符串,可以在抛出之前修改为说出您喜欢的内容。但是,完全替换错误 stack 属性可能会让调试非常混乱。

当原始抛出的错误和错误处理程序位于不同的位置或文件(这在 Promise 中很常见)时,您可能能够追踪原始错误的来源,但无法追踪实际捕获错误的处理程序。为避免这种情况,最好在stack 中保留对原始错误和新错误的一些引用。如果其中存储了其他元数据,访问完整的原始错误也很有用。

这是一个捕获错误的示例,将其包装在一个新错误中,但添加原始 stack 并存储 error

try {
  throw new Error('First one')
} catch (error) {
  let e = new Error(`Rethrowing the "${error.message}" error`)
  e.original_error = error
  e.stack = e.stack.split('\n').slice(0,2).join('\n') + '\n' +
            error.stack
  throw e
}

哪个抛出:

/so/42754270/test.js:9
    throw e
    ^

Error: Rethrowing the "First one" error
    at test (/so/42754270/test.js:5:13)
Error: First one
    at test (/so/42754270/test.js:3:11)
    at Object.<anonymous> (/so/42754270/test.js:13:1)
    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)
    at run (bootstrap_node.js:394:7)
    at startup (bootstrap_node.js:149:9)

所以我们创建了一个新的通用Error。不幸的是,原始错误的类型从输出中隐藏了,但error 已附加为.original_error,因此仍然可以访问它。新的stack 已被大部分删除,除了生成线很重要,并附加了原始错误stack

任何尝试解析堆栈跟踪的工具都可能不适用于此更改或最佳情况,它们会检测到两个错误。

使用 ES2015+ 错误类重新抛出

把它变成一个可重用的 ES2015+ 错误类:

class RethrownError extends Error {
  constructor(message, error){
    super(message)
    this.name = this.constructor.name
    if (!error) throw new Error('RethrownError requires a message and error')
    this.original_error = error
    this.stack_before_rethrow = this.stack
    const message_lines =  (this.message.match(/\n/g)||[]).length + 1
    this.stack = this.stack.split('\n').slice(0, message_lines+1).join('\n') + '\n' +
                 error.stack
  }
}

throw new RethrownError(`Oh no a "${error.message}" error`, error)

结果

/so/42754270/test2.js:31
    throw new RethrownError(`Oh no a "${error.message}"" error`, error)
    ^

RethrownError: Oh no a "First one" error
    at test (/so/42754270/test2.js:31:11)
Error: First one
    at test (/so/42754270/test2.js:29:11)
    at Object.<anonymous> (/so/42754270/test2.js:35:1)
    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)
    at run (bootstrap_node.js:394:7)
    at startup (bootstrap_node.js:149:9)

那么您就知道,每当您看到 RethrownError 时,原始错误仍将在 .original_error 处出现。

这种方法并不完美,但这意味着我可以将来自底层模块的已知错误重新键入更容易处理的泛型类型,通常使用 bluebirds filtered catch .catch(TypeError, handler)

注意 stack 在此处变为可枚举

修改堆栈时出现相同错误

有时您需要将原始错误保持原样。

在这种情况下,您只需将新信息附加/插入到现有堆栈中即可。

file = '/home/jim/plumbers'
try {
   JSON.parse('k')
} catch (e) {
   let message = `JSON parse error in ${file}`
   let stack = new Error(message).stack
   e.stack = e.stack + '\nFrom previous ' + stack.split('\n').slice(0,2).join('\n') + '\n'
   throw e
}

返回

/so/42754270/throw_error_replace_stack.js:13
       throw e
       ^

SyntaxError: Unexpected token k in JSON at position 0
    at Object.parse (native)
    at Object.<anonymous> (/so/42754270/throw_error_replace_stack.js:8:13)
    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)
    at run (bootstrap_node.js:394:7)
    at startup (bootstrap_node.js:149:9)
From previous Error: JSON parse error in "/home/jim/plumbers"
    at Object.<anonymous> (/so/42754270/throw_error_replace_stack.js:11:20)

还请注意,堆栈处理很简单,并且假定错误消息是单行。如果您遇到多行错误消息,您可能需要查找 \n at 来终止消息。

【讨论】:

  • 为什么需要captureStackTrace?我没有发现任何情况会有所作为。
  • 嗯,可能不再相关了。自从它最初是在早期的 ES6 中编写的以来,我并没有改变太多。如果记忆有用,那是 ES5 转译的事情,在 v8 浏览器中从堆栈顶部删除自定义 Error 代码。有一点related discussion here
【解决方案2】:

如果您只想更改消息,则只需更改消息即可:

try {
  throw new Error("Original Error");
} catch(err) {
  err.message = "Here is some context -- " + err.message;
  throw err;
}

更新:

如果消息属性是只读的,您可以使用原始错误作为原型创建一个新对象并分配一个新消息:

try {  // line 12
  document.querySelectorAll("div:foo");  // Throws a DOMException (invalid selector)
} catch(err) {
  let message = "Here is some context -- " + err.message;
  let e = Object.create( err, { message: { value: message } } );
  throw e;  // line 17
}

不幸的是,记录的异常消息只是“未捕获的异常”,没有异常附带的消息,因此创建错误并为其提供相同的堆栈可能会有所帮助,这样记录的消息将包含错误消息:

try {  // line 12
  document.querySelectorAll("div:foo");  // Throws a DOMException (invalid selector)
} catch(err) {
  e = new Error( "Here is some context -- " + err.message );
  e.stack = err.stack;
  throw e;  // line 17
}

由于 sn -p 输出显示了重新抛出的行号,这证实了堆栈被保留:

try {  // line 12
  try {  // line 13
    document.querySelectorAll("div:foo");  // Throws a DOMException (invalid selector)
  } catch(err) {
    console.log( "Stack starts with message: ", err.stack.split("\n")[0] );
    console.log( "Inner catch from:", err.stack.split("\n")[1] );
    e = new Error( "Here is some context -- " + err.message );  // line 18
    console.log( "New error from:", e.stack.split("\n")[1] );
    e.stack = err.stack;
    throw e;  // line 21
  }
} catch(err) {
  console.log( "Outer catch from:", err.stack.split("\n")[1] );
  throw err;  // line 25
}

【讨论】:

  • 某些错误,如 DOMException,具有只读消息属性。
  • @Aaronius,我从未见过,DOMException 听起来像是浏览器的东西而不是 Node 的东西。但如果这是一个问题,我接下来要做的就是创建一个使用原始错误作为原型的新对象;我没有测试过这个,但是像e = Object.create(err); e.message = "Here is some context -- " + err.message; throw e
  • 这导致TypeError: Cannot set property message of which has only a getter。可能有办法做到这一点,但我还没有深入挖掘。您关于 DOMException 不在 Node 中的观点是正确的。我将删除我的反对票。 Node 中可能存在其他类型的错误,但 message 属性同样是只读的。
  • 我刚刚看到您的最新更新。我需要给他们一个机会。
【解决方案3】:

你也可以继续在你的 try 链中抛出错误。如果您想修改任何内容,请在此过程中进行:在 b 中的 throw 语句之前。

function a() {
    throw new Error('my message');
}

function b() {
    try {
        a();
    } catch (e) {
        // add / modify properties here
        throw e;
    }
}

function c() {
    try {
        b();
    } catch (e) {
        console.log(e);
        document.getElementById('logger').innerHTML = e.stack;
    }
}
c();
&lt;pre id="logger"&gt;&lt;/pre&gt;

【讨论】:

    【解决方案4】:

    您可能想看看来自 Joyent 的 verror module,它提供了一种简单的错误包装方法:

    var originError = new Error('No such file or directory');
    var err = new VError(originError, 'Failed to load configuration');
    console.error(err.message);
    

    这将打印:

    Failed to load configuration: No such file or directory
    

    【讨论】:

    • 这会保留调用堆栈吗?
    猜你喜欢
    • 2010-11-09
    • 2011-07-12
    • 1970-01-01
    • 1970-01-01
    • 2011-06-01
    • 2019-06-07
    • 1970-01-01
    • 2016-04-27
    相关资源
    最近更新 更多