Repository navigation
Promise readline question results in unsettled promise on abortion #53497
Description
Activity
More Additional Information
According to the Nodejs documentation related to exit codes:
13 Unsettled Top-Level Await: await was used outside of a function in the top-level code, but the passed Promise never settled.
In the example below I expect to get
13as an exit code inbeforeExitandexitevents but it doesn't.import { createInterface } from "node:readline/promises"; process.on("exit", (code) => { console.log("\nexit:", code); }); process.on("beforeExit", (code) => { console.log("\nbeforeExit:", code); }); process.on("warning", (w) => { console.log("\nwar :", w); }); const rl = createInterface({ input: process.stdin, output: process.stdout }); await rl.question("Enter something:");
Result :
Enter something: beforeExit: 0 exit: 0 Warning: Detected unsettled top-level await at file:///home/imanhpr/Desktop/sandbox/node-box/sig.js:16 const data = await rl.question("Enter something:");
It's good to mention when I check the previous process exit code with
echo $?it's13and it's fine.Reacted by WilliSI could try to work on this. Can someone assign me?
Hi! If you have a solution, feel free to submit a PR!
- addedreadlineIssues and PRs related to the built-in readline module.Issues and PRs related to the built-in readline module.promisesIssues and PRs related to ECMAScript promises.Issues and PRs related to ECMAScript promises.
on Jun 24, 2024 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..?@jahudka Can you share a code snippet?
@DanielVenable sure thing, this is a minimal example which demonstrates what happens:
import { createInterface } from 'node:readline/promises'; import { stdin, stdout } from 'node:process'; const rl = createInterface({ input: stdin, output: stdout }); const ctrl = new AbortController(); process.on('SIGINT', function handleSignal() { console.log(`This should be printed when you press Ctrl+C, but it won't be`); process.off('SIGINT', handleSignal); ctrl.abort(); }); try { const response = await rl.question('Press Ctrl+C now!', { signal: ctrl.signal }); console.log('This is never reached if you press Ctrl+C'); } catch { console.log('And neither is this'); } console.log('Nor this'); rl.close();
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.0@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() {.@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!
- added a commit that references this issue
on Jan 30, 2025 - added a commit that references this issue
on Feb 2, 2025 - added a commit that references this issue
on Apr 2, 2025 - added a commit that references this issue
on Aug 18, 2025 8 remaining items
- added 2 commits that reference this issue
on Dec 23, 2025 - added 7 commits that reference this issue
on Jan 14, 2026 - added a commit that references this issue
on Jan 16, 2026 - added a commit that references this issue
on Jun 13, 2026 - added a commit that references this issue
on Jul 3, 2026 - added 2 commits that reference this issue
on Sep 1, 2026 - added a commit that references this issue
on Sep 12, 2026
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:
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:
What do you see instead?
The promise remains unsettled, resulting in a warning that cannot be caught:
Additional information
This issue is opened based on the discussion in [stackoverflow] How to properly abort Node readline promise question?