await does not call Promise.then method in Node version > 10

Viewed 253

The await keyword calls the Promise.then method in NodeJS <= v10 but this behavior is not observed in v12 or v14 or higher. I want a trap/hook for then .then method that gets invoked irrespective of await or explicit .then style callbacks. I was able to achieve this in Node <= 10 but does not work in Node 12 or 14 (randomly chosen). Am I doing something wrong?

➜  Desktop cat test.js

async function func() {
  const aPromise = new Promise((resolve, reject) => {
    setTimeout(() => { resolve(123); }, 100);
  });

  const clone = Object.create(aPromise);
  const _then = Object.getPrototypeOf(clone).then;
  _then.bind(aPromise);

  aPromise.then = function () {
    console.log('then called');
    return _then.apply(this, arguments);
  };

  const result = await aPromise;
  console.log('Log:', result);
}

func();%

➜  Desktop
➜  Desktop nvm use 10
Now using node v10.19.0 (npm v6.13.4)
➜  Desktop
➜  Desktop
➜  Desktop node test.js
then called
Log: 123
➜  Desktop
➜  Desktop
➜  Desktop nvm use 12
Now using node v12.18.4 (npm v6.14.6)
➜  Desktop
➜  Desktop
➜  Desktop node test.js
Log: 123
➜  Desktop
➜  Desktop
➜  Desktop nvm use 14
Now using node v14.12.0 (npm v6.14.8)
➜  Desktop
➜  Desktop
➜  Desktop node test.js
Log: 123
➜  Desktop
1 Answers

There was as normative change to the spec in how await handles promises created via the built-in Promise constructor that allowed optimization. If you want to hook into the addition of handlers to the promise, you can use a subclass to do that:

class MyPromise extends Promise {
    then(onFulfilled, onRejected) {
        console.log("then called");
        return super.then(onFulfilled, onRejected);
    }
}
async function func() {
    const aPromise = new MyPromise((resolve, reject) => {
        setTimeout(() => { resolve(123); }, 100);
    });
  
    const result = await aPromise;
    console.log("Log:", result);
}
func();

That works even on version of V8 with the optimization (for instance, Node.js v14):

then called
Log: 123

If you need to do this with a promise you've received rather than created, you can create an instance of your own Promise subclass and resolve it to that other promise using the static resolve method of your subclass:

class MyPromise extends Promise {
    then(onFulfilled, onRejected) {
        console.log("then called");
        return super.then(onFulfilled, onRejected);
    }
}
async function func() {
    // I'm creating it here, but this is to simulate having gotten
    // a promise from somewhere you don't control
    const aPromise = new Promise((resolve, reject) => {
        setTimeout(() => { resolve(123); }, 100);
    });

    // Create an instance of your subclass and resolve it to the other promise
    const result = await MyPromise.resolve(aPromise);
    console.log('Log:', result);
}
func();

That also shows:

then called
Log: 123
Related