Chrome-remote-interface: How to know where unhandled promise was triggered (Error: Promise was collected)?

Created on 25 Apr 2017  路  24Comments  路  Source: cyrus-and/chrome-remote-interface

Somewhere in my code I have a promise where I'm not handling a reject. It's giving me the error:

Logging in
JQMIGRATE: Migrate is installed with logging active, version 1.4.1
(node:1172) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): Error: Promise was collected
(node:1172) DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
Logging in
Go to NEXT page

I looked through my code a few times but didn't see an issue. The error in question is somewhere in this block:

  function login() {
    return new Promise((resolve, reject) => {
      try {
        var userEl = 'input.username';
        var passEl = 'input.password';
        document.querySelector(userEl).value = 'username';
        document.querySelector(passEl).value = 'password';
        $('button.submit').click();
        setTimeout(resolve, 2000)
      } catch (e) {
        return reject(e);
      }
    });
  }

And the block of code that triggers this is in a Runtime.evaluate block:

...
   let state = 'login';
    Page.loadEventFired(async() => {
      switch (state) {
        case 'login':
          console.log('Logging in');
          let res = await Runtime.evaluate({
              expression: `(${login})()`,
              awaitPromise: true,
            })
          state = 'navigateNextPage';
          console.log('Go to NEXT page');
          await Page.navigate({
            url: 'nextPageUrl',
          });
          break;
        case 'navigateNextPage':
        ...
           break;
        ...
...
some code to navigate to first login page
...

Seems like in the error, the first time the login invokes in the client, it fails, then the next run is successful. Is there a way to somehow print the stack trace whenever an unhandled Promise rejection occurs?

question

Most helpful comment

Try with node --trace-warnings ? It'll give you a stack

All 24 comments

Try with node --trace-warnings ? It'll give you a stack

@paulirish: thanks I'll try it out. Didn't know node added this feature from V6+.

@paulirish Awww, I've always attempted to use --trace-deprecation with great frustration...

@pthieu Let me know if you find the source of your issue because that error (Promise was collected) seems to be quite unusual if you Google for it, yet this is the second issue that mentions it (#89).

--trace-warnings works great for me but it has to be used BEFORE the file name:

node --trace-warnings file.js

and not

node file.js --trace-warnings # this won't work

@vvo it's your script's first argument in the latter case.

@cyrus-and: my memory is vague but I may have seen this once in a completely unrelated script I was writing. I may be an Node.js thing (V7+?), not so much this repo.

not so much this repo

@pthieu Yes, it's not about this repo; still curious though.

@cyrus-and: yep let me give you a ping if I figure it out.

Update: --trace-warnings works great. Will try to get a good stacktrace today.

@cyrus-and: got the stack trace, looks like this:

$ node --trace-warnings chrome.js
Logging in
(node:8100) Error: Promise was collected
    at f:\www\tmp\node_modules\chrome-remote-interface\lib\chrome.js:56:28
    at Chrome.handleMessage (f:\www\tmp\node_modules\chrome-remote-interface\lib\chrome.js:232:13)
    at WebSocket.<anonymous> (f:\www\tmp\node_modules\chrome-remote-interface\lib\chrome.js:209:27)
    at emitTwo (events.js:106:13)
    at WebSocket.emit (events.js:194:7)
    at Receiver._receiver.onmessage (f:\www\tmp\node_modules\ws\lib\WebSocket.js:143:54)
    at Receiver.dataMessage (f:\www\tmp\node_modules\ws\lib\Receiver.js:385:14)
    at extension.decompress (f:\www\tmp\node_modules\ws\lib\Receiver.js:354:40)
    at _inflate.flush (f:\www\tmp\node_modules\ws\lib\PerMessageDeflate.js:279:12)
    at afterWrite (_stream_writable.js:383:3)
    at onwrite (_stream_writable.js:374:7)
(node:8100) DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
    at emitWarning (internal/process/promises.js:69:15)
    at emitPendingUnhandledRejections (internal/process/promises.js:86:11)
    at runMicrotasksCallback (internal/process/next_tick.js:67:9)
    at _combinedTickCallback (internal/process/next_tick.js:73:7)
    at process._tickCallback (internal/process/next_tick.js:104:9)
Logging in
Go to Next page
... seems to have restarted ...

Here's this snippet of code where it fails:

    Page.loadEventFired(async() => {
      switch (state) {
        case 'login':
          console.log('Logging in');
          let res = await Runtime.evaluate({
              expression: `(${login})()`,
              awaitPromise: true,
            })
            // console.log('response:', res);
          state = 'afterLogin';
          console.log('Go to Next page');
          await Page.navigate({
            url: '<next link>',
          });
          break;
        case 'afterLogin':
          ...
          break;
      }
    });

and my login function looks like:

  function login() {
    return new Promise((resolve, reject) => {
      try {
        var userEl = 'input.username';
        var passEl = 'input.password';
        document.querySelector(userEl).value = '<username>';
        document.querySelector(passEl).value = '<password>';
        $('button.submit').click();
        setTimeout(resolve, 2000)
      } catch (e) {
        return reject(e);
      }
    });
  }

I'm handling the reject so not sure why it's complaining that it's "unhandled". Maybe the setTimeout is changing the scope here, and that's where it's breaking?

@pthieu

I'm handling the reject so not sure why it's complaining that it's "unhandled". Maybe the setTimeout is changing the scope here, and that's where it's breaking?

The login function is executed in the browser context so Node.js cannot complain about unhandled promise rejections there.

What version of chrome-remote-interface are you using? So I can properly map the line numbers.

@cyrus-and: That makes sense. I'll get you the version in a few hours when I get home, but for the time being, is there a way to handle errors from the browser context when doing:

            Runtime.evaluate({
              expression: `(${login})()`,
              awaitPromise: true,
            })

Perhaps either wrapping the entire thing in a try-catch or having a property onError or even onAwaitError if people want to be explicit?

@pthieu JavaScript errors in the browser context are not errors for what concerns the CDP, meaning that Runtime.evaluate will not fail. If you want to catch errors from the browser context you'll have to inspect the returned objects: result and the optional exceptionDetails.

@cyrus-and:

$ npm ls | grep remote
+-- [email protected]

@pthieu thanks for the info, unfortunately I cannot understand what is causing Error: Promise was collected. If you manage to come up with a minimal snippet which reproduces it feel free to post it here.

That error seems to be caused when the promise hasn't still been resolved or rejected and the document is unloaded.
A hidden redirect was causing that in my case.

I hit the same situation in another script I was writing. I was playing around with AWS DynamoDB:

main();

async function main() {
  var data = await createTable()
    .catch((e) => {
      console.log('createTable() failed: %j', e);
    });
}

async function createTable() {
  return new Promise((resolve, reject) => {
    dynamoDb.createTable(params, function(err, data) {
      if (err) {
        console.error('Unable to create table. Error JSON:',
          JSON.stringify(err, null, 2));
        return reject(err);
      } else {
        console.log('Created table. Table description JSON:',
          JSON.stringify(data, null, 2));
        return resolve(data);
      }
    });
  });
}

In the main() function, if i didn't have the .catch(), the same thing warning will show.

Unfortunately, in my previous code, I'm returning a promise which is handled by chrome-remote-interface.

I may be able to handle the .catch() within the login() function, right when I instantiate the promise, but maybe the better option is to allow for catching of errors thrown by the library itself.

Here's the smallest snippet I could write to reproduce this issue.

process.on('unhandledRejection', e => console.error('UNHANDLED PROMISE', e));

const CDP = require('chrome-remote-interface');
const expression = 'new Promise(resolve => document.querySelector("h1").innerText)';

(async function() {
    const protocol = await CDP({port: 9222});

    const {Page, Runtime} = protocol;
    await Promise.all([Page.enable(), Runtime.enable()]);

    Page.navigate({url: 'http://example.com/'});

    Page.loadEventFired(async () => {
        const result = await Runtime.evaluate({expression, awaitPromise: true});

        console.log('data', result);

        protocol.close();
    });
})();

I was expecting a result with the "Example Domain" string in it, instead I got:

UNHANDLED PROMISE { Error: Promise was collected
    at /usr/src/app/node_modules/chrome-remote-interface/lib/chrome.js:81:28
    at Chrome.handleMessage (/usr/src/app/node_modules/chrome-remote-interface/lib/chrome.js:271:13)
    at WebSocket.<anonymous> (/usr/src/app/node_modules/chrome-remote-interface/lib/chrome.js:248:27)
    at emitTwo (events.js:125:13)
    at WebSocket.emit (events.js:213:7)
    at Receiver._receiver.onmessage (/usr/src/app/node_modules/ws/lib/WebSocket.js:143:54)
    at Receiver.dataMessage (/usr/src/app/node_modules/ws/lib/Receiver.js:385:14)
    at extension.decompress (/usr/src/app/node_modules/ws/lib/Receiver.js:354:40)
    at _inflate.flush (/usr/src/app/node_modules/ws/lib/PerMessageDeflate.js:279:12)
    at afterWrite (_stream_writable.js:430:3) code: -32000 }

Any reason you guys could think why this is happening?

I think I finally understood what's happening here: the promise gets collected by the garbage collector when the JavaScript execution environment is invalidated, e.g., a page is navigated or reloaded.

You can easily reproduce it like this:

$ chrome-remote-interface inspect
>>> Runtime.evaluate({expression: `new Promise(() => {})`, awaitPromise: true})
>>> Page.reload() // then wait several seconds
{ result: {} }
{ error: { code: -32000, message: 'Promise was collected' } }

Just make sure there are no pending promises before closing, reloading, etc. a page.

@fracasula in you example that promise simply never resolves, try with:

const expression = 'new Promise(resolve => resolve(document.querySelector("h1").innerText))';

It makes a huge amount of sense. It works now thanks @cyrus-and :+1:

In fact I think I initially got this while I was trying to read a specific element in a page after clicking a link that triggers a page reload.

For example in the example.com page we click on "More information..." and then we want to read the content of the new page (in this case iana.org). Do you think is doable?

Thanks in advance!

@fracasula see #106, in particular this comment.

Hi @cyrus-and

I think I finally understood what's happening here: the promise gets collected by the garbage collector when the JavaScript execution environment is invalidated, e.g., a page is navigated or reloaded.

I think it should be mentioned in the readme because it is strange error with not clear message.

@mahnunchik I added a FAQ entry.

Was this page helpful?
0 / 5 - 0 ratings