enquirer icon indicating copy to clipboard operation
enquirer copied to clipboard

Docs: Explain cancellation better

Open pke opened this issue 4 years ago • 4 comments

I still don't get how cancellation works with prompts.

try {
  await prompt({
    type: "input",
    message: "Test",
    name: "test"
  })
} catch (e) {
  console.error(e)
}

Pressing Ctrl+C will throw an empty String. How am I supposed to know that the prompt has been cancelled? Or to ask it differently, under which circumstance can the prompt throw too so I can distinguish between cancel (empty string) and other exceptions?

pke avatar Nov 01 '19 10:11 pke

I spent hours debugging my app to figure out what was throwing an empty string and why.

I get that ctrl+c will reject the prompt promise, but it would be helpful if it rejected with an Error, a stack trace, and a code identifying the action.

As a workaround, just need to add cancel(){} to my prompt options to prevent the "cancel" event from being emitted and suppress the rejection.

cb1kenobi avatar Aug 01 '20 05:08 cb1kenobi

Can you explain a bit more in detail you workaround please? Beside, I think this project is dead and I already switched to "prompts" which is maintained.

pke avatar Aug 01 '20 20:08 pke

When you press ctrl+c, the keypress event handler calls Prompt.keypress():

async keypress(input, event = {}) {
    this.keypressed = true;
    let key = keypress.action(input, keypress(input, event), this.options.actions);
    this.state.keypress = key;
    this.emit('keypress', input, key);
    this.emit('state', this.state.clone());
    let fn = this.options[key.action] || this[key.action] || this.dispatch;
    if (typeof fn === 'function') {
      return await fn.call(this, input, key);
    }
    this.alert();
 }

First it identifies the action as cancel.

Next it checks if the options contains a method matching that event, in this case "cancel". If you don't have a cancel() {}, it will dispatch the ctrl+c and do the default "cancel" behavior which ultimately rejects the promise returned by prompt().

Below is the built-in "cancel" behavior.

async cancel(err) {
    this.state.cancelled = this.state.submitted = true;

    await this.render();
    await this.close();

    if (typeof this.options.onCancel === 'function') {
      await this.options.onCancel.call(this, this.name, this.value, this);
    }

    this.emit('cancel', await this.error(err));
 }

When you ctrl+c, the above cancel() is passed err that is a string containing "\x0003", the ASCII control character for ctrl+c. The string "\x0003" is then formatted via this.error():

 error(err) {
    return !this.state.submitted ? (err || this.state.error) : '';
}

A cancelled prompt will have its state set to submitted which will cause this.error() tol throw away the "\x0003" and simply return '' (empty string).

Then back in cancel(), it emits the "cancel" event with an empty string which in turn triggers the promise returned by Prompt.run() to reject with an empty string.

It would be nice if Prompt.run() forced the emitted "cancel" value to be an error like this:

// potential fix in Prompt.run()
this.once('cancel', err => {
    if (!(err instanceof Error)) {
        err = new Error(err || 'Prompting cancelled'); // default message instead of empty string
    }
    err.code = 'ECANCELLED'; // or something
    reject err;
});

Hope that makes sense and maybe a PR to change the behavior is agreeable.

cb1kenobi avatar Aug 03 '20 17:08 cb1kenobi

Agreed, I think the current behavior is incorrect. A PR would be great, I'm still getting caught up so my apologies in advance if one was already submitted.

jonschlinkert avatar Jul 28 '23 12:07 jonschlinkert