How do I invoke inquirer.js menu in a loop using Promises?

Issue

I wrote a simple Node.js program with a nice menu system facilitated by inquirer.js. However, after selecting an option in the menu and completing some action, the program exits. I need the menu to show again, until I select the Exit [last] option in the menu. I would like to do this using Promise, instead of async/await.

I tried using a function to show the menu and called that function within a forever loop (E.g. while (true) { ... }), but that made the program unusable. I changed that to a for-loop just to observe the problem. Below is the simple program and the resulting output.

PROGRAM

"use strict";

const inquirer = require('inquirer');
const util = require('util')

// Clear the screen
process.stdout.write("\u001b[2J\u001b[0;0H");

const showMenu = () => {
  const questions = [
    {
      type: "list",
      name: "action",
      message: "What do you want to do?",
      choices: [
        { name: "action 1", value: "Action1" },
        { name: "action 2", value: "Action2" },
        { name: "Exit program", value: "quit"}
      ]
    }
  ];
  return inquirer.prompt(questions);
};

const main = () => {
  for (let count = 0; count < 3; count++) {
    showMenu()
    .then(answers => {
      if (answers.action === 'Action1') {
        return Promise.resolve('hello world');
      }
      else if (answers.action === 'Action2') {
        return new Promise((resolve, reject) => {
          inquirer
            .prompt([
              {
                type: 'input',
                name: 'secretCode',
                message: "Enter a secret code:"
              }
            ])
            .then(answers => {
              resolve(answers);
            })
        });
      }
      else {
        console.log('Exiting program.')
        process.exit(0);
      }
    })
    .then((data) => { console.log(util.inspect(data, { showHidden: false, depth: null })); })
    .catch((error, response) => {
      console.error('Error:', error);
    });
  }
}

main()

OUTPUT

? What do you want to do? (Use arrow keys)
❯ action 1
  action 2
  Exit program ? What do you want to do? (Use arrow keys)
❯ action 1
  action 2
  Exit program ? What do you want to do? (Use arrow keys)
❯ action 1
  action 2
  Exit program (node:983) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 keypress listeners added to [ReadStream]. Use emitter.setMaxListeners() to increase limit

How can I block after the first call to generate the menu, wait for an option to be selected and the corresponding action to complete, and then cycle back to the next iteration of showing the menu?

Solution

You can use async/await syntax:

Declare your main function async, and await the returned Promise from inquirer:

const main = async () => {
  for (let count = 0; count < 3; count++) {
    await showMenu()
    .then(answers => {
      [...]
  }
};

Your code doesn’t work as you expect because, in short, the interpreter executes synchronous code before running any callbacks (from promises). As a consequence your synchronous for loop executes before any I/O callbacks are resolved. All calls to showMenu() returns promises which are resolved asynchronously, meaning nothing will be printed, and no inputs will be interpreted until after looping.

Writing await blocks succeeding synchronous code inside an async function, which is what it seems you’re trying to do.

Answered By – aksomat

Answer Checked By – Candace Johnson (AngularFixing Volunteer)

Leave a Reply

Your email address will not be published.