'How to change Promise constructor in Node.js project running locally?

This might sound strange, but I would like to alter my local Node.js version and modify the Promise implementation to add a new source instance property.

global.Promise = class SourcePromise extends Promise {
  constructor(params) {
    super(params)
    this.source = new Error('This is where this promise was created').stack
  }
}

This would be to help me debug an error occurring on my Nuxt app, but only on the server. I'm able to catch the error by listening to the unhandledRejection event, but the error returned is not an Error object, it is simply undefined so I have no clue where it's coming from. The callback of unhandledRejection also returns the promise so I tried to add the code snippet above at the very beginning of nuxt start script to be able to log the source like:

process.on('unhandledRejection', (error, promise) => {
  console.log('Unhandled Rejection:', error?.stack)
  console.log('Promise source:', promise.source)
})

but promise.source is also undefined. If I log console.log(Promise.resolve().source) from any script, it works and I get the source so the only explanation I have in mind would be that the promise is created in a child process where my Promise extension is not defined.

To sum up, since it's happening in a separate process and I can't identify which one, the only way I see of implementing my SourcePromise globally in all Node processes would be to change the Promise definition directly in my local version of Node. Is it even possible?

I'm on macOS Monterey 12.3.1 using nvm v0.38.0

EDIT

I ended up cloning Node.js from Github to be able to build it locally and use it to start my Nuxt server. The problem is: it's written in C++ which I don't understand. I think I found where the promise constructor is defined which calls NewJsPromise that seems to be defined here, but I'll need help from a C++ developer since I still don't know how or where to add the stack...



Solution 1:[1]

(V8 developer here.) Bad news up front: I don't know exactly how to do what you're asking, and I don't have the time to figure it out for you.

But since you're not getting other answers so far, I'll try to say a few things that might help you make progress:

  • I don't think this is a "separate process" issue. JS objects, including Promises, can't travel between processes. If you see it in one process, then it was created in that same process.
  • There's more than one piece of code in V8 that creates Promises, so to be sure that you'll catch the promise in question, you'll have to update all of them:
    1. NewJSPromise in src/builtins/promise-misc.tq, which you've found, is the Torque (not C++!) implementation, used for both the JavaScript Promise constructor and several other builtins. Note that if you put your modifications only into the PromiseConstructor, that would skip the other uses of the helper, so be sure to update NewJSPromise.
    2. There's the C++ version in Factory::NewJSPromise in src/heap/factory.cc (which is used, probably among other things, for V8's API, i.e. any case where Node itself or a custom Node add-on creates Promises).
    3. There's inlining support in the optimizing compiler in PromiseBuiltinReducerAssembler::ReducePromiseConstructor in src/compiler/js-call-reducer.cc (which could potentially be dealt with by disabling compiler support for inlining Promise creation).
  • The general outline of what you'd do is:
    1. Update the Promise object definition to include another field.
    2. Look at how Error objects are created to see how to get the stack. Error objects employ some fancy "lazy creation" scheme for stack traces for performance reasons; it may be easier to follow that example (to minimize divergence), or it may be easier to simplify it (because you don't care about performance). Note that AFAICT Error objects are always created in C++; you'd have to figure out how to get stacks to Torque-created Promises (off the top of my head I don't have a good suggestion for that).
    3. Update all places where Promises are created (see above) to initialize the new field accordingly.
  • I strongly suspect that there are less time consuming ways to debug your original problem. (I dunno, maybe just audit all places where you're dealing with promises, and check that they all have rejection handlers? That might take hours, but quite possibly fewer hours than the V8 modification project sketched above.)

Good luck with your investigation!

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 jmrk