Skip to content

Fix unhandled rejection from process.stdin.close() in execProcess (#14445)#14451

Merged
gordonwoodhull merged 2 commits intomainfrom
bugfix/14445-execProcess-no-await
Apr 28, 2026
Merged

Fix unhandled rejection from process.stdin.close() in execProcess (#14445)#14451
gordonwoodhull merged 2 commits intomainfrom
bugfix/14445-execProcess-no-await

Conversation

@gordonwoodhull
Copy link
Copy Markdown
Member

@gordonwoodhull gordonwoodhull commented Apr 28, 2026

Summary

Fixes #14445 — intermittent Uncaught (in promise) TypeError: Writable stream is closed or errored. aborting renders on Linux.

src/core/process.ts:97 called process.stdin.close() without awaiting the returned Promise. If the child closes/errors its stdin before the parent's close completes, the Promise rejects with Writable stream is closed or errored. Because it was fire-and-forget, the rejection escaped execProcess's try/catch and surfaced on a later microtask as an uncaught Deno rejection — bypassing the try/catch in analyzeNeededPackages that 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.tsawait process.stdin.close() inside a try/catch. The child's exit status already reflects any real failure, so the close rejection is not actionable from execProcess.
  • src/command/render/output-typst.ts — surface the captured stderr in the fallback warning from analyzeNeededPackages, so the next user with a broken typst-gather binary can diagnose without objdump.
  • tests/unit/exec-process-stdin.test.ts — regression test running four execProcess scenarios 1000 iterations each with a global unhandledrejection listener that asserts no rejection fires. The first scenario (child exits without reading) is the one that reproduced on Ubuntu pre-fix.

Test plan

  • Unit tests pass on macOS arm64 (./run-fast-tests.sh unit/exec-process-stdin.test.ts, 31s, 4 passed)
  • Regression test reproduced the bug on Ubuntu CI before the fix (2/200 unhandled rejections in scenario 1)
  • Same regression test passes on Ubuntu CI after the fix

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.
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
@gordonwoodhull gordonwoodhull marked this pull request as ready for review April 28, 2026 12:35
@gordonwoodhull gordonwoodhull changed the title Reproducer test for #14445 (unawaited stdin.close in execProcess) Fix unhandled rejection from process.stdin.close() in execProcess (#14445) Apr 28, 2026
@gordonwoodhull gordonwoodhull merged commit 769a28a into main Apr 28, 2026
51 checks passed
@gordonwoodhull gordonwoodhull deleted the bugfix/14445-execProcess-no-await branch April 28, 2026 14:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

typst-gather binary not compatible with Ubuntu 22.04 (GLIBC 2.38 floor)

1 participant