readline/promises: reject instead of throwing synchronously from Interface#question - #33742
readline/promises: reject instead of throwing synchronously from Interface#question#33742robobun wants to merge 1 commit into
Conversation
…rface#question Wrap the whole body in the Promise executor like Node does, so validateAbortSignal() and [kQuestion]'s ERR_USE_AFTER_CLOSE throw become rejections instead of escaping synchronously past the .catch() callers attach. The already-aborted fast path now goes through reject() as well.
|
Warning Review limit reached
Next review available in: 14 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (2)
Comment |
|
This PR may be a duplicate of:
🤖 Generated with Claude Code |
|
Closing as a duplicate of #33344, which already wraps |
There was a problem hiding this comment.
LGTM — straightforward Node compat fix wrapping the method body in the Promise executor so validation/close errors reject instead of throwing synchronously.
Extended reasoning...
Overview
This PR modifies a single method, PromisesInterface.question() in src/js/node/readline.ts, to wrap its entire body inside new Promise((resolve, reject) => {...}) instead of using $newPromiseCapability with top-level calls. The logic is otherwise unchanged — same signal validation, same abort-listener wiring, same this[kQuestion] call — but now any synchronous throw (from validateAbortSignal or ERR_USE_AFTER_CLOSE in [kQuestion]) is captured by the executor and delivered as a rejection, matching Node.js. It also replaces the detached PromiseReject(...) on the already-aborted fast path with reject(...), fixing a latent |this| is not an object TypeError. Three tests are added to test/js/node/readline/readline_promises.node.test.ts covering the closed-interface, invalid-signal, and already-aborted-signal paths.
Security risks
None. This is a pure error-delivery-mechanism change in a user-facing readline API — no auth, crypto, filesystem, or network surface is touched, and no new inputs are accepted.
Level of scrutiny
Low. The diff is ~20 lines in one method of a Node-compat JS builtin, and the transformation is mechanical (move existing statements inside an executor). The arrow-function executor correctly preserves this for this[kQuestionCancel]() and this[kQuestion](query, cb). This is exactly how Node implements the same method, so there is no design ambiguity.
Other factors
The bug-hunting system found no issues. Tests are hermetic (PassThrough input, no network/timers), assert specific error name/code/cause, and clean up via try/finally. The PR description documents that all three tests fail on main and pass with the change, and that the existing 79-test readline suite still passes. No CODEOWNERS cover this path and there are no outstanding human review comments.
Repro
Node prints
caught ERR_USE_AFTER_CLOSE. Bun throws synchronously before.catchis attached:Same for an invalid
options.signal(ERR_INVALID_ARG_TYPE).question()is a promise API, so callers handle errors with.catch()ortry { await }inside an async function that is usually not on the stack at the call site. Any caller racing aclose()(a SIGINT handler, a'line'handler that closes the interface, an abortedcreateInterface({ signal })) gets an uncaught synchronous exception where Node delivers a handled rejection.Cause
Node evaluates the whole
question()body insidenew Promise((resolve, reject) => { ... }), sovalidateAbortSignal()'s throw and[kQuestion]'sERR_USE_AFTER_CLOSEthrow are converted to rejections by the executor. Bun'sPromisesInterface.questionbuilt its capability with$newPromiseCapabilityand calledvalidateAbortSignal(signal, ...)andthis[kQuestion](query, cb)at the top level of the method, so both throws escaped synchronously.Fix
Match Node: wrap the whole body in the executor. This also routes the already-aborted fast path through
reject()instead of the detachedPromiseReject(that receiver bug is tracked more broadly in #33343, which also covers theutil.promisify(rl.question)sibling and unrelated call sites).Verification
Three tests added to
test/js/node/readline/readline_promises.node.test.ts: closed interface (ERR_USE_AFTER_CLOSE), invalidoptions.signal(ERR_INVALID_ARG_TYPE), and an already-aborted signal (AbortError/ABORT_ERRwithcausepreserved). All three fail on main (the first two with a synchronous throw, the third withTypeError: |this| is not an object) and pass with this change. The existingreadline.node.test.tssuite (79 tests) continues to pass.