NodeJS的promise.catch和console.log存在bug?

5
运行以下代码时,如果我将console.log("fnError: ", fnError)注释掉或不注释掉,会得到不同的结果。这对我来说似乎非常奇怪。
天哪,一个对console.log的调用如何影响我的promise?
function run() {
    var fn = function(){
        throw new Error("incorrect message");
    };

    // returns a promise that should fail with
    // an error object whose .message is "correct message"
    var promisifiedFn = function(){
        return Promise.resolve()
            .then(fn)
            .catch((fnError) => {
                // commenting this out fixes things! //
                console.log("fnError: ", fnError);
                ///////////////////////////////////////

                fnError.message = "correct message";
                throw fnError;
            })
    }

    promisifiedFn().catch((e) => {
        console.log("caught error.message:", e.message);
        console.log("caught error:", e);
    });
}
run();

以上代码的输出结果为:
// fnError:  Error: incorrect message
//     at fn (/Users/sam/dev/ethereum/pennyeth/js/temp.js:18:9)
//     at <anonymous>
//     at process._tickCallback (internal/process/next_tick.js:169:7)
//     ...
// caught error.message: correct message
// caught error: Error: incorrect message
//     at fn (/Users/sam/dev/ethereum/pennyeth/js/temp.js:18:9)
//     at <anonymous>
//     at process._tickCallback (internal/process/next_tick.js:169:7)
//     ...

注意到“错误信息”被记录在日志中。如果您取消注释console.log("fnError: ", fnError),您将得到以下结果:

// caught error.message: correct message
// caught error: Error: correct message
//     at fn (/Users/sam/dev/ethereum/pennyeth/js/temp.js:18:9)
//     at <anonymous>
//     at process._tickCallback (internal/process/next_tick.js:169:7)
//     ....

运行 Node 8.0.0


注意:在我的实际应用程序代码中,我在重新抛出错误之前 没有 记录错误,并且我仍然得到这种行为。不幸的是,我无法找出一个最小的示例。 - JS_Riddler
这很奇怪。我在Chrome 59中尝试过它,但它没有同样的问题。但Node 7.9.0、Node 8.0.0和Node 8.1.2都有这个行为(或bug)。 - styfle
1
我唯一的想法是console.log在渲染到stdout之前缓存了Error对象。请注意,两个“不正确的消息”结果都记录了相同的对象。 - styfle
我的应用程序代码仍然存在问题,即使我没有使用 console.log,仍然得到错误的错误对象。我不得不抛出一个新的错误,但这会丢失原始的堆栈跟踪。 - JS_Riddler
1
请执行 console.log(Object.getOwnPropertyDescriptor(fnError, "message")) 吗? - Bergi
@Bergi { value: 'first', writable: true, enumerable: false, configurable: true } - styfle
2个回答

2

这是预期的行为。

记录错误(至少通过util.inspectString)会评估其.stack属性。当错误对象被实例化时,堆栈字符串未初始化,而是懒惰地构建以节省内存。堆栈跟踪将包括错误消息,并且当您更改.message属性时,它将反映在堆栈跟踪中或不反映,具体取决于是否已经创建了堆栈跟踪。

来自V8堆栈跟踪API描述

为了提高效率,在捕获堆栈跟踪时不会格式化堆栈跟踪,而是按需生成,即第一次访问堆栈属性时。

以及来自官方node Error文档

表示堆栈跟踪的字符串在访问error.stack属性时才会懒惰地生成。


为什么Chrome的行为与Node.js不同?是Node.js进行了这种优化而Chrome没有吗? - styfle
@styfle 我还没有尝试过,但我怀疑区别只在于 console.log 而不是 error.stack - Bergi
1
@styfle 当明确访问.stack时,我可以在Chrome中重现它。但是,仅使用console.log(e)确实会显示更新的消息。 - Bergi
这让我真的想念Java了...而且你可以链接异常,永远不会遇到这种混乱。 - JS_Riddler
@Bergi 是否有其他事件会导致 stack 被构建?即使不执行 console.log,我仍然遇到相同的行为,尽管我还无法将其简化为一个示例。 - JS_Riddler
与Java不同,异常的堆栈跟踪是一个结构化值,允许检查堆栈状态;而在V8中,堆栈属性只包含格式化的堆栈跟踪的平面字符串,这仅仅是为了与其他浏览器兼容。据我所知,已经有努力标准化结构化堆栈跟踪API,但由于没有迫切需要,它们尚未被实现。 - Bergi

1
我能够用以下只有四行的代码重现这个“bug”。
var e = new Error('first');
console.log(e);
e.message = 'new';
console.log(e);

我在Chrome 59中尝试了它,没有出现同样的问题。然而,Node 7.9.0、 Node 8.0.0和Node 8.1.2都存在这个问题。我在GitHub上报告了一个错误#13832,所以我们会看到会发生什么。更新1:为了证明这不是一个时间问题,我们可以添加setTimeout调用。
var e = new Error('first');
console.log(e);
setTimeout(() => { e.message = 'new'; }, 1000);
setTimeout(() => console.log(e), 2000);

问题仍然存在,即使我们等待调用console.log(),这让我相信输出已被缓存。 更新2:我从GitHub上得到了mscdex的回复:

这是预期的,因为您看到的是堆栈跟踪,其中包括生成错误消息后的错误消息。堆栈跟踪是惰性生成的,并且仅生成一次(出于性能原因),因此您会看到两次相同的输出。

但是,如果您将代码更改为输出e.message,则会看到预期的输出更改。

最终,mscdexBergi都表示根本原因是惰性评估。
您所做的可能不是常见情况,因此我建议将来避免这样做,因为node.js团队似乎不会更改此行为。

我在我的应用程序代码中仍然遇到这个问题,它做了类似的事情,但没有执行“console.log”,这意味着我得到了“旧”的错误消息。 - JS_Riddler

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接