Skip to content

Modifying error.message does not update error.stack if stream.destroy(error) has been called #51715

Description

@ehmicky

Version

v21.1.0

Platform

Linux ether-laptop 6.5.0-15-generic #15-Ubuntu SMP PREEMPT_DYNAMIC Tue Jan 9 17:03:36 UTC 2024 x86_64 x86_64 x86_64 GNU/Linux

Subsystem

No response

What steps will reproduce the bug?

import {Writable} from 'node:stream'

const error = new Error('This failed')
const stream = new Writable()
stream.destroy(error)
stream.on('error', () => {})
error.message = `Additional info: ${error.message}`
// This does not print 'Additional info', but should
console.log(error)
// Error: This failed
//  at file:///...
//  ...

How often does it reproduce? Is there a required condition?

No.

What is the expected behavior? Why is that the expected behavior?

Printed error should show "Additional info".

What do you see instead?

Printed error does not show "Additional info".

Additional information

This is due to #34103, specifically this:

Always stringify error.stack, because the small performance penalty that comes with it is worth the reduced risk of memory leaks. (I’ve confirmed that this also fixes the test with the patch below.)

As implemented in:

err.stack; // eslint-disable-line no-unused-expressions

If error.message is modified later on (e.g. due to prepending some additional information), the change won't be reflected with error.stack. This is unfortunate because error.stack is used by util.inspect(), which is itself used by console.log().

Activity

  1. climba03003 commented on Feb 10, 2024

    @climba03003
    Contributor

    Shouldn't it be reported to V8 instead?

  2. marco-ippolito commented on Feb 10, 2024

    @marco-ippolito
    Member

    It seems to be working correctly on Chrome

  3. ehmicky commented on Feb 10, 2024

    @ehmicky
    Author

    Chrome does not have stream.destroy(), so cannot be impacted by this bug.

    error.stack is memoized by V8. However, this is intentional and not a bug. This issue is not reporting error.stack memoization, but the improper usage of that memoization.

    The bug is that Node.js has the following line inside stream.destroy(error) in order to intentionally use that memoization:

    if (err) {
    // Avoid V8 leak, https://github.com/nodejs/node/pull/34103#issuecomment-652002364
    err.stack; // eslint-disable-line no-unused-expressions

    This workaround was meant for the unit tests, but it is creating user-facing problems.

  4. climba03003 commented on Feb 11, 2024

    @climba03003
    Contributor

    @marco-ippolito

    I don't think it works in Chrome.
    If it works in Chrome, it will works in Node.js.

    err = Error('foo')
    err.stack
    err.message = `bar ${err.message}`
    console.log(err)

    Chrome 121.0.6167.161
    image

    FireFox
    image

    This issue is not reporting error.stack memoization, but the improper usage of that memoization.

    I think it is a bug in V8 because the memorization of stack trace variables leads to the user cannot properly cache or recycle the error when it dispose (it is the cause of memory leak). The problem it tends to stop is not only related to the test case, but also the user environment.

    If the trick err.stack pre-calculation cannot be used, then the internal should never cache the Error since it is unsafe to do so.

  5. ehmicky commented on Feb 11, 2024

    @ehmicky
    Author

    Memoizing error.stack in V8 is not a bug, it is a performance feature, which has been around for many years. Therefore it is unlikely to be removed by the V8 team.

    The bug being reported relates to the Node.js stream API, in particular stream.destroy(), and is not related to Chrome.

    What can be fixed though is the workaround highlighted in my initial message. It appears that this workaround was intended to fix some automated tests, but it unfortunately creates user-facing issues.

  6. climba03003 commented on Feb 11, 2024

    @climba03003
    Contributor

    What can be fixed though is the workaround highlighted in my initial message. It appears that this workaround was intended to fix some automated tests, but it unfortunately creates user-facing issues.

    If the memory leak exists in test, which means it will also happen in user code.
    So, I don't think it only happen in test only.
    The proper fix means stream should keep the error in reference.

    If the memory leak did not pop up in first place, there is no need of workaround.
    So, it is a bug in V8 which cause memory leak. Isn't it?

  7. ehmicky commented on Feb 12, 2024

    @ehmicky
    Author

    Yes, that's a good point about this not being only an issue with the automated tests, but a memory leak which could potentially be experienced by users too.

    error.stack memoization with V8 has been around for around 7 years. I have actually written a few libraries to work around this specific issue (set-error-message , set-error-stack and modern-errors).

    The V8 implementation might introduce a memory leak per @addaleax comment:

    What’s happening is that the error.stack property refers to the full stack frames of its creation. V8 does this to be able to compute error.stack lazily as a string, instead of always formatting it directly even if it isn’t used. Those stack frames in turn can refer to the actual values of variables in that stack frame.

    However, if this is the case, it is unclear how many years for this bug to be solved. It is also unclear to me whether the v8 team would agree that this is a bug. As mentioned by @addaleax:

    Ask the V8 team for a solution to this. [...] The big downside is that this probably takes some time to implement.

    The approach in #34103 has not been to wait for the potential V8 bug to be fixed, but instead of find a workaround right away. That workaround has a problem which (I think) might not have been anticipated based on @addaleax comment:

    Always stringify error.stack, because the small performance penalty that comes with it is worth the reduced risk of memory leaks.

    Beyond the small performance penalty, this workaround introduces a bigger problem: modifying error.message does not update error.stack. Modifying error.message in catch block to add some information is a common practice. It seems like this side effect might have been unforeseen, as it is not mentioned in the PR.

    I am curious whether a different workaround exists that would not have this side effect. 🤔

  8. added
    confirmed-bugIssues and PRs for confirmed bugs.
    v8 engineIssues and PRs related to the V8 dependency.
    on Sep 12, 2025
  9. BridgeAR commented on Sep 12, 2025

    @BridgeAR
    Member

    @nodejs/v8 would someone be able to check if we could just recalculate the stack part that is related to the error message and error name? I believe the caching is mostly important for the stack frames. The message itself should probably be possible to still change.

  10. camillobruni commented on Sep 15, 2025

    @camillobruni
    Contributor

    This was apparently fixed in https://crrev.com/c/5378709 back in 2024-04-04.

  11. BridgeAR commented on Sep 15, 2025

    @BridgeAR
    Member

    @camillobruni I feel that made it worse, not better. A new message will now never be picked up instead of when it's not yet accessed 😢. The bug report did suggest what I suggested which seems to also align with Firefox and Safari.

  12. ehmicky commented on Sep 15, 2025

    @ehmicky
    Author

    This new V8 behavior was just introduced in Node 24.5.0, which is probably why this issue is getting new activity. For clarity:

    • Before, the message in error.stack would reflect error.message at the time error.stack is first accessed
    • Now, the message in error.stack reflects error.message at the time new Error() is constructed. That is, even if error.message has been modified since.

    In the meantime, I have created the libraries set-error-message and wrap-error-message to work around this problem.

    This does mean though that the original issue is now a V8 problem, not a Node problem anymore. Originally the error.message change would not be reflected in error.stack in a Node-specific situation, i.e. when stream.destroy(error) has been called, due to Node accessing error.stack early. However, now any error.message change is never reflected in error.stack, regardless of the situation. From that perspective, it does not make the behavior more consistent, which is what their goal was.

    Based on this, I am closing this, since this is now a V8 issue.

  13. BridgeAR commented on Sep 15, 2025

    @BridgeAR
    Member

    I think we should keep track of this and keep this open. The current behavior is very tricky for users.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    confirmed-bugIssues and PRs for confirmed bugs.v8 engineIssues and PRs related to the V8 dependency.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions