Fix unhandled rejection from process.stdin.close() in execProcess (#14445)#14451
Merged
gordonwoodhull merged 2 commits intomainfrom Apr 28, 2026
Merged
Fix unhandled rejection from process.stdin.close() in execProcess (#14445)#14451gordonwoodhull merged 2 commits intomainfrom
gordonwoodhull merged 2 commits intomainfrom
Conversation
execProcess() in src/core/process.ts calls process.stdin.close() without awaiting the returned Promise. When the child closes/errors its stdin first, that Promise rejects with "Writable stream is closed or errored." and surfaces as an uncaught rejection that aborts the render — bypassing the try/catch in analyzeNeededPackages. These tests run many iterations of execProcess scenarios that exercise the same code path and assert no unhandled rejection fires. Pass on macOS arm64; expected to fail on Ubuntu CI if the diagnosis is correct.
3 tasks
execProcess() in src/core/process.ts called process.stdin.close() without awaiting the returned Promise. When the child closes/errors its stdin before the parent's close completes, the close Promise rejects with "Writable stream is closed or errored." Because the Promise was unhandled, the rejection escaped the surrounding try/catch and surfaced on a later microtask as an uncaught Deno rejection that aborted the render — bypassing the try/catch in analyzeNeededPackages that was meant to fall back gracefully. Manifests on Linux at roughly a 1% race rate when typst-gather analyze runs against a broken or fast-failing input. Confirmed on Ubuntu CI; not reproduced on macOS arm64. The fix is to await the close() inside a try/catch — the child's exit status reflects any real failure, so the close rejection is not actionable from execProcess. Also surface the captured stderr in the fallback warning from analyzeNeededPackages, so the next user with a broken typst-gather binary can diagnose without objdump. Includes a regression test that runs four execProcess scenarios 1000 iterations each with a global unhandledrejection listener and asserts no rejection fires. Fixes #14445
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes #14445 — intermittent
Uncaught (in promise) TypeError: Writable stream is closed or errored.aborting renders on Linux.src/core/process.ts:97calledprocess.stdin.close()without awaiting the returned Promise. If the child closes/errors its stdin before the parent's close completes, the Promise rejects withWritable stream is closed or errored.Because it was fire-and-forget, the rejection escapedexecProcess'stry/catchand surfaced on a later microtask as an uncaught Deno rejection — bypassing thetry/catchinanalyzeNeededPackagesthat was meant to fall back gracefully when typst-gather analyze fails.Reproduced on Ubuntu CI at roughly a 1% race rate via the child exits without reading stdin scenario in the regression test (commit 48199c3, bucket 11 job log). Not reproduced on macOS arm64.
Changes
src/core/process.ts—await process.stdin.close()inside atry/catch. The child's exit status already reflects any real failure, so the close rejection is not actionable fromexecProcess.src/command/render/output-typst.ts— surface the captured stderr in the fallback warning fromanalyzeNeededPackages, so the next user with a broken typst-gather binary can diagnose withoutobjdump.tests/unit/exec-process-stdin.test.ts— regression test running fourexecProcessscenarios 1000 iterations each with a globalunhandledrejectionlistener that asserts no rejection fires. The first scenario (child exits without reading) is the one that reproduced on Ubuntu pre-fix.Test plan
./run-fast-tests.sh unit/exec-process-stdin.test.ts, 31s, 4 passed)