Promise readline question results in unsettled promise on abortionΒ #53497
Copy link
Copy link
Closed
Labels
promisesIssues and PRs related to ECMAScript promises.Issues and PRs related to ECMAScript promises.readlineIssues and PRs related to the built-in readline module.Issues and PRs related to the built-in readline module.
Description
Version
v22.2.0
Platform
Linux LX-LAPII 6.9.3-arch1-1 #1 SMP PREEMPT_DYNAMIC Fri, 31 May 2024 15:14:45 +0000 x86_64 GNU/Linux
Subsystem
node:readline/promises
What steps will reproduce the bug?
It is not possible to properly handle a user's abortion of a promise readline question with SIGINT or Ctrl+D while rl.question waits for user input:
import * as readline from 'node:readline/promises';
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
try {
const answer = await rl.question('>>> ');
} catch {}
How often does it reproduce? Is there a required condition?
Every time when a prompt is aborted with Ctrl+C or Ctrl+D.
What is the expected behavior? Why is that the expected behavior?
The expected behavior is to settle a promise on a readline close event, as shown in the following snippet:
import * as readline from 'node:readline';
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
let rej = () => {};
rl.on('close', () => {
console.log('closing');
rej();
});
const answer = await new Promise((r, j) => {
rej = j;
rl.question('>>> ', r);
}).catch(() => console.error('interrupted'));
What do you see instead?
The promise remains unsettled, resulting in a warning that cannot be caught:
>>> Warning: Detected unsettled top-level await at file:///tmp/bug.mjs:9
const answer = await rl.question('>>> ');
Additional information
This issue is opened based on the discussion in [stackoverflow] How to properly abort Node readline promise question?
Reacted by Matt Johnson-Pint, msv-ttd and YAMAMOTO Yuji
Metadata
Metadata
Assignees
Labels
promisesIssues and PRs related to ECMAScript promises.Issues and PRs related to ECMAScript promises.readlineIssues and PRs related to the built-in readline module.Issues and PRs related to the built-in readline module.
Activity
imanhpr commentedon Jun 18, 2024
More Additional Information
According to the Nodejs documentation related to exit codes:
In the example below I expect to get
13as an exit code inbeforeExitandexitevents but it doesn't.Result :
It's good to mention when I check the previous process exit code with
echo $?it's13and it's fine.DanielVenable commentedon Jun 24, 2024
I could try to work on this. Can someone assign me?
avivkeller commentedon Jun 24, 2024
Hi! If you have a solution, feel free to submit a PR!
jahudka commentedon Jan 23, 2025
Hi, just ran across this; funny thing is,
rl.question()accepts anAbortSignalin the second argument, but when I tried installing aSIGINThandler and aborting the question manually, I got exactly the same result as before - meaning my signal handler wasn't even called. I hope I'm not overlooking something, but it looks as though therl.question()method somehow hijacks the signal..?DanielVenable commentedon Jan 24, 2025
@jahudka Can you share a code snippet?
jahudka commentedon Jan 25, 2025
@DanielVenable sure thing, this is a minimal example which demonstrates what happens:
shove that into a
test.mjsfile and run it withnode test.mjs; after pressing Ctrl+C, only the "Warning: Detected unsettled top-level await" is printed and the script is immediately terminatednode -v:v23.3.0DanielVenable commentedon Jan 25, 2025
@jahudka It is not
rl.questionbutcreateInterfacethat hijacks the SIGINT signal. OncecreateInterfaceis called, the first SIGINT closes the readline interface. In your example that causes the process to exit because there is nothing left keeping it open. If there was something keeping it open, a second SIGINT would then callhandleSignal. To make your example work as expected, replaceprocess.on('SIGINT', function handleSignal() {withrl.on('SIGINT', function handleSignal() {.jahudka commentedon Jan 25, 2025
@DanielVenable Oh, wow, okay, makes sense. Never would've guessed that Readline would mess with signal handling like that.. But seeing the docs now and the fact that it handles
SIGTSTPandSIGCONTit makes sense why it would do that. Still, I'd say it's somewhat unexpected and that it would be perhaps better to have those signal handlers be opt-in; or even just to have an option to opt out, which I don't see in the docs.Anyway, I've just confirmed that if you install a
SIGINThandler on the readline interface rather than on the process itself, you can abort the.question()call gracefully using an AbortController as shown in my example code and the warning will not be shown (although an AbortError will be thrown - but that can easily be caught).So as far as I'm concerned, this could maybe do with a big fat warning in the docs, but otherwise I'd say it's working as intended. Thanks for helping me debug!
readline: fix unresolved promise on abortion
readline: fix unresolved promise on abortion
readline: fix unresolved promise on abortion
readline: fix unresolved promise on abortion
8 remaining items
fix: suppress readline close errors in enquirer adapter for Node.js 22+
fix: skip enquirer cancel callback tests on Node.js 24+
fix: suppress readline close errors in enquirer adapter for Node.js 22+
fix: skip enquirer cancel callback tests on Node.js 24+
perf(deps): update major dependencies (#754)
chore(release): 10.0.0 [skip ci]
chore(release): 4.0.0 [skip ci]
chore(release): 4.0.0 [skip ci]
chore(release): 4.0.0 [skip ci]
Update dependency listr2 to v10 (#2277)
Epic AQJ95G: default namespace .project/ β resolver, setup scaffold, β¦
feat(cli): styled prompts, single-letter choice(), clean Ctrl+C
ci: run each test file in its own node process
ci: run each test file in its own node process (#3)
fix(create-mono-stack): stop the unsettled top-level await warning onβ¦