Title: webstreams: writer.closed settles before sink abort() completes when first observed during erroring · Issue #65726 · nodejs/node · GitHub
Open Graph Title: webstreams: writer.closed settles before sink abort() completes when first observed during erroring · Issue #65726 · nodejs/node
X Title: webstreams: writer.closed settles before sink abort() completes when first observed during erroring · Issue #65726 · nodejs/node
Description: Version v24.19.0 through v24.20.0, and current main (v24.18.1 behaves per spec) Platform All (verified on Linux x64) Subsystem webstreams What steps will reproduce the bug? Abort a WritableStream whose sink has a pending operation, and a...
Open Graph Description: Version v24.19.0 through v24.20.0, and current main (v24.18.1 behaves per spec) Platform All (verified on Linux x64) Subsystem webstreams What steps will reproduce the bug? Abort a WritableStream w...
X Description: Version v24.19.0 through v24.20.0, and current main (v24.18.1 behaves per spec) Platform All (verified on Linux x64) Subsystem webstreams What steps will reproduce the bug? Abort a WritableStream w...
Opengraph URL: https://github.com/nodejs/node/issues/65726
X: @github
Domain: github.com
{"@context":"https://schema.org","@type":"DiscussionForumPosting","headline":"webstreams: writer.closed settles before sink abort() completes when first observed during erroring","articleBody":"### Version\n\nv24.19.0 through v24.20.0, and current `main` (v24.18.1 behaves per spec)\n\n### Platform\n\nAll (verified on Linux x64)\n\n### Subsystem\n\nwebstreams\n\n### What steps will reproduce the bug?\n\nAbort a `WritableStream` whose sink has a pending operation, and access `writer.closed` for the first time *after* erroring has begun but *before* the sink's `abort()` promise settles:\n\n```js\n'use strict';\nconst pending = [];\nfunction deferred() {\n const d = {};\n d.promise = new Promise((res, rej) => { d.resolve = res; d.reject = rej; });\n return d;\n}\n\nconst ws = new WritableStream({\n write() { const d = deferred(); pending.push(d); return d.promise; },\n abort() { const d = deferred(); pending.push(d); return d.promise; },\n});\nconst w = ws.getWriter();\n\nw.write('chunk').catch(() => {});\nw.abort(new Error('boom')).catch(() => {});\n\nsetTimeout(async () => {\n // First access to writer.closed: sink.abort() has NOT settled yet\n // (its promise is parked in `pending`).\n const sentinel = {};\n let state;\n try {\n state = (await Promise.race([w.closed, Promise.resolve(sentinel)])) === sentinel\n ? 'pending' : 'fulfilled';\n } catch (e) { state = `rejected: ${e.message}`; }\n console.log(process.version,\n 'writer.closed while sink abort() pending:', state,\n '| sink abort settled:', pending.length === 0);\n process.exit(0);\n}, 10);\n```\n\nOutput:\n\n```\nv24.18.1 writer.closed while sink abort() pending: pending | sink abort settled: false\nv24.19.0 writer.closed while sink abort() pending: rejected: boom | sink abort settled: false\nv24.20.0 writer.closed while sink abort() pending: rejected: boom | sink abort settled: false\n```\n\nNote the divergence only occurs when `writer.closed` is *first* accessed after erroring begins. If the getter was already touched while the stream was `'writable'`, all versions follow spec timing — which makes this easy to miss in tests that eagerly attach `closed` handlers.\n\n### How often does it reproduce? Is there a required condition?\n\nDeterministic. Requires the first access of `writer.closed` to happen in the window where the stream has begun erroring but the sink's `abort()` promise has not settled.\n\n### What is the expected behavior? Why is that the expected behavior?\n\n`writer.closed` must remain **pending** until the sink's `abort()` promise settles.\n\nPer the spec, `writer.[[closedPromise]]` is rejected only in [`WritableStreamRejectCloseAndClosedPromiseIfNeeded`](https://streams.spec.whatwg.org/#writable-stream-reject-close-and-closed-promise-if-needed), which [`WritableStreamFinishErroring`](https://streams.spec.whatwg.org/#writable-stream-finish-erroring) invokes only from the *upon fulfillment/rejection* reactions of the promise returned by the sink's `abortAlgorithm`. The state is set to `\"errored\"` earlier in FinishErroring, so \"state is errored\" and \"closedPromise is rejected\" are deliberately not the same instant: user code awaiting `writer.closed` is guaranteed the sink's abort/cleanup has completed. Node ≤ 24.18.1 and the spec reference implementation both behave this way.\n\n### What do you see instead?\n\nOn v24.19.0+ a `writer.closed` first observed in that window comes back **already rejected** (with the stored error), i.e. it settles before the sink's abort/cleanup has finished. Code that uses `await writer.closed.catch(...)` as the \"teardown complete\" signal now proceeds while `abort()` is still running.\n\n### Additional information\n\nCause: v24.19.0's lazy materialization of the writer's `[[closedPromise]]` record (`writerClosedPromise()` in `lib/internal/webstreams/writablestream.js`, introduced in #63876). The lazy derivation maps stream state `'errored'` directly to an already-rejected record:\n\n```js\nswitch (stream[kState].state) {\n case 'writable':\n case 'erroring':\n close = PromiseWithResolvers();\n break;\n case 'closed':\n close = resolvedRecord();\n break;\n default: // 'errored' — but the spec sets 'errored' BEFORE the sink abort runs\n close = rejectedHandledRecord(stream[kState].storedError);\n}\n```\n\nbut the spec reaches state `\"errored\"` while the deferred rejection point (abort settlement) is still in the future, so deriving \"rejected\" from the state alone is incorrect for exactly the FinishErroring-with-pending-abort window.\n\nRelated: on v24.20.0 this same early-materialized record (which has `reject: undefined`) caused an uncaught internal `TypeError: closeCache.reject is not a function` in `writableStreamRejectCloseAndClosedPromiseIfNeeded` when the abort later settled, crashing the process. That crash was fixed on `main` by #64825 (staged for v24.x), but the fix guards the settle call with `isPromisePending()` and leaves the early rejection in place — `writerClosedPromise()` is unchanged on current `main`, so the spec-conformance issue described here survives the crash fix.\n","author":{"url":"https://github.com/trevnorris","@type":"Person","name":"trevnorris"},"datePublished":"2026-09-01T18:55:57Z","url":"https://github.com/nodejs/node/issues/65726"}
| react-profiling | 0 |
| react-import-map | react,react-dom,react-dom/client,react-dom/profiling,react-is,react-reconciler,react/compiler-runtime,react/jsx-dev-runtime,react/jsx-runtime,scheduler@d5750093facf |
| route-pattern | /_view_fragments/issues/show/:user_id/:repository/:id/issue_layout(.:format) |
| route-controller | voltron_issues_fragments |
| route-action | issue_layout |
| fetch-nonce | v2:15514183-6d56-304f-a040-3162f3c78f20 |
| current-catalog-service-hash | 81bb79d38c15960b92d99bca9288a9108c7a47b18f2423d0f6438c5b7bcd2114 |
| request-id | 9B46:30551B:50AE1:6DF4B:6AED03FB |
| html-safe-nonce | 9ee2ec5bbdee34aa4b2bdcd43861330782ae4705a98d4338005a1092159e68a5 |
| visitor-payload | eyJyZWZlcnJlciI6IiIsInJlcXVlc3RfaWQiOiI5QjQ2OjMwNTUxQjo1MEFFMTo2REY0Qjo2QUVEMDNGQiIsInZpc2l0b3JfaWQiOiI5NjQyOTY3MTc1Mzc5NjkxNDciLCJyZWdpb25fZWRnZSI6ImlhZCIsInJlZ2lvbl9yZW5kZXIiOiJpYWQifQ== |
| visitor-hmac | 4747853f22184884ab9e656dd33c5d300e3d769c32ab0e39a378697ea8c656f7 |
| hovercard-subject-tag | issue:5315630608 |
| github-keyboard-shortcuts | repository,issues,copilot |
| google-site-verification | Apib7-x98H0j5cPqHWwSMm6dNU4GmODRoqxLiDzdx9I |
| octolytics-url | https://collector.github.com/github/collect |
| analytics-location | / |
| fb:app_id | 1401488693436528 |
| apple-itunes-app | app-id=1477376905, app-argument=https://github.com/_view_fragments/issues/show/nodejs/node/65726/issue_layout |
| twitter:image | https://opengraph.githubassets.com/4ef25ed98260a90a42a6f4d9f3041fccdcf021c8d53edead67f4ee79813d07c5/nodejs/node/issues/65726 |
| twitter:card | summary_large_image |
| og:image | https://opengraph.githubassets.com/4ef25ed98260a90a42a6f4d9f3041fccdcf021c8d53edead67f4ee79813d07c5/nodejs/node/issues/65726 |
| og:image:alt | Version v24.19.0 through v24.20.0, and current main (v24.18.1 behaves per spec) Platform All (verified on Linux x64) Subsystem webstreams What steps will reproduce the bug? Abort a WritableStream w... |
| og:image:width | 1200 |
| og:image:height | 600 |
| og:site_name | GitHub |
| og:type | object |
| og:author:username | trevnorris |
| hostname | github.com |
| expected-hostname | github.com |
| None | 21e10c535436b2122cfb44c76dfb4af296e371850fb60de16776f8a64e5c3ed8 |
| turbo-cache-control | no-preview |
| go-import | github.com/nodejs/node git https://github.com/nodejs/node.git |
| octolytics-dimension-user_id | 9950313 |
| octolytics-dimension-user_login | nodejs |
| octolytics-dimension-repository_id | 27193779 |
| octolytics-dimension-repository_nwo | nodejs/node |
| octolytics-dimension-repository_public | true |
| octolytics-dimension-repository_is_fork | false |
| octolytics-dimension-repository_network_root_id | 27193779 |
| octolytics-dimension-repository_network_root_nwo | nodejs/node |
| turbo-body-classes | logged-out env-production page-responsive |
| disable-turbo | false |
| browser-stats-url | https://api.github.com/_private/browser/stats |
| browser-errors-url | https://api.github.com/_private/browser/errors |
| release | 302a6d707c3288a5f951258a2fa3e926b8c8f8cf |
| ui-target | full |
| theme-color | #1e2327 |
| color-scheme | light dark |
| react-app-name | issues |
Links:
Viewport: width=device-width