Dmitry Papka
Dmitry Papka

Reputation: 1359

Is Puppeteer making my NodeJS process to exit?

I'm playing with Puppeteer and wrote this example (should probably never happen in production, but still):

const puppeteer = require('puppeteer');

(async () => {
  // creating browser instance and closing it
  const browser = await puppeteer.launch({ headless: false })
  browser.disconnect()
  await browser.close()

  console.log('first check') // everything is ok here, message is printed

  // opening page on the closed browser instance. Just for testing purposes
  const page = await browser.newPage()
  await page.goto('http://google.com')

  // never printed
  console.log('second check')
})()

So basically, I am trying to create a new page on a closed instance of the browser. Obviously, no page is opening because browser instance is closed. But I am expecting some error. Instead nothing happens and the second console.log is never executed!.

Question. If no error is thrown, why does the program never reach the second console.log? Does puppeteer somehow closes the process of my NodeJS application? Or I am missing something?

puppeteer version: latest - 5.3.1 (also 3.0.0)

By the way, if I use some earlier puppeteer version (2.0.0), same code is failing with error as I expect:

Error: WebSocket is not open: readyState 2 (CLOSING)

Update.

After debugging a bit the internals of Puppeteer I found out the following:

They have a Connection class with the map of callbacks as a property. Whenever we call the newPage method, a connection with new id is created as well as a new corresponding Promise. This promise resolve and reject functions are assigned to the callbacks map:

send(method, ...paramArgs) {
    const params = paramArgs.length ? paramArgs[0] : undefined;
    const id = this._rawSend({ method, params });
    return new Promise((resolve, reject) => {
          this._callbacks.set(id, { resolve, reject, error: new Error(), method });
    });
}

Then, the Connection class has the _onMessage(message) callback. Whenever some data (message) is received, they inspect the message to find out if it is an OK or an ERROR message. After this they invoke the stored resolve or reject callback.

But since the browser instance is my example is already closed, the message never arrives and the Promise is neither resolved nor rejected.

And after small research, I found out that NodeJS is not able to track such a Promises. Example:

(async () => {
  const promise = new Promise((resolve, reject) => {
    if (true === false) {
      resolve(13) // this will never happen
    }
  })

  const value = await promise
  console.log(value) // we never come here
})()

Upvotes: 1

Views: 2644

Answers (2)

Mark Finn
Mark Finn

Reputation: 53

I agree that this seems to be a bug. I see the issue you made and added a potential fix.

Adding this as the first thing in Connection.send() seems to fix the issue:

if (this._closed)
            return Promise.reject(new Error(`Protocol error (${method}): Target closed.`));

In the mean time, I have added this to my code so at least it doesn't die silently with no indication that it failed:

process.on('beforeExit', (code) => {
        //beforeExit will run if out of callbacks, but not on an exit()
        console.log('We seem to be exiting purely because there are no more awaits scheduled instead of having reached and exit.  Assuming this is bad behavior from the browser process. previous exit code: ', code);
        process.exit(1); 
});


//my code goes here
asdf()

process.exit(0);//will exit without triggering the beforeExit message.

Honestly the behavior of Node in silently exiting seems like it is a little lacking. You can set an exitCode, but having a program completely able to run up to an await then die silently without triggering exception handlers or finally blocks is a little gross.

Upvotes: 2

Sam R.
Sam R.

Reputation: 16450

You don't see any error probably because you don't wait for the async function to settle. If you attach a catch handler most likely you'll catch the error:

const puppeteer = require('puppeteer');

(async () => {
  // creating browser instance and closing it
  const browser = await puppeteer.launch({ headless: false })
  browser.disconnect()
  await browser.close()

  console.log('first check') // everything is ok here, message is printed

  // opening page on the closed browser instance. Just for testing purposes
  const page = await browser.newPage()
  await page.goto('http://google.com')

  // never printed
  console.log('second check')
})()
.then(() => console.log('done'))
.catch(e => console.error(e)); // <= HERE

Or use try/catch:

const puppeteer = require("puppeteer");

(async () => {
  try {
    // creating browser instance and closing it
    const browser = await puppeteer.launch({ headless: false });
    browser.disconnect();
    await browser.close();

    console.log("first check"); // everything is ok here, message is printed

    // opening page on the closed browser instance. Just for testing purposes
    const page = await browser.newPage();
    await page.goto("http://google.com");

    // never printed
    console.log("second check");
  } catch (e) {
    console.error(e);
  }
})();

Upvotes: 0

Related Questions