Skip to content

Bug: SIGINT handling prevents asynchronous operations from continuing #1741

@JSierraAKAMC

Description

@JSierraAKAMC

When following the suggestions for handling ctrl+c gracefully, I've found that trying to await some asynchronous code after handling the ExitPromptError will result in an unresolved top-level await error.

Example:
repro.ts

import {ExitPromptError} from '@inquirer/core';
import {input} from '@inquirer/prompts';

async function sleep(ms: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

async function main(): Promise<void> {
  try {
    const name = await input({message: 'What is your name?'});
    console.log('Hello', name);
  } catch (err) {
    if (err instanceof ExitPromptError) {
      console.log('I respect your privacy. Give me a second.');
      await sleep(1000);
    } else {
      throw err;
    }
  }
  console.log("Okay, let's begin.");
}

await main();

When answering the prompt, everything works as expected:

% node --disable-warning=ExperimentalWarning ./repro.ts
✔ What is your name? Foobar
Hello Foobar
Okay, let's begin.

However, when using ctrl+c in the prompt, I get an unsettled top-level await error:

% node --disable-warning=ExperimentalWarning ./repro.ts
? What is your name? Warning: Detected unsettled top-level await at file:///path/to/repro.ts:25
await main();
^




I respect your privacy. Give me a second.

I've tried adding a custom SIGINT listener with process.on('SIGINT', sigintHandler), removing all existing SIGINT listeners, and everything in between to no avail.

However, I did find that adding a handler for the readline interface's SIGINT event in createPrompt()of @inquirer/core seemed to "fix" the problem:

    /* --------------- EXISTING CODE --------------- */
    return withHooks(rl, (cycle) => {
            // The close event triggers immediately when the user press ctrl+c. SignalExit on the other hand
            // triggers after the process is done (which happens after timeouts are done triggering.)
            // We triggers the hooks cleanup phase on rl `close` so active timeouts can be cleared.
            const hooksCleanup = AsyncResource.bind(() => effectScheduler.clearAll());
            rl.on('close', hooksCleanup);
            cleanups.add(() => rl.removeListener('close', hooksCleanup));

            /* --------------- NEW CODE --------------- */
            const sigint = () => reject(new ExitPromptError(`User force closed the prompt with SIGINT`));
            rl.on('SIGINT', sigint);
            cleanups.add(() => rl.removeListener('SIGINT', sigint));

When pressing ctrl+c with the patched version:

% node --disable-warning=ExperimentalWarning ./repro.ts              
? What is your name?
I respect your privacy. Give me a second.
Okay, let's begin.

However, I don't have enough context to know if this is the "correct" fix.


For context, I've tested this on Node 22 and 24 with @inquirer/prompts versions 7.2.1 and 7.5.0.

If any other information is needed, please let me know.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions