Title: `readable.find()`/`some()`/`every()` don't destroy the stream on match when the source hasn't ended · Issue #64261 · nodejs/node · GitHub
Open Graph Title: `readable.find()`/`some()`/`every()` don't destroy the stream on match when the source hasn't ended · Issue #64261 · nodejs/node
X Title: `readable.find()`/`some()`/`every()` don't destroy the stream on match when the source hasn't ended · Issue #64261 · nodejs/node
Description: Version v24.18.0 (reproduces on this version; likely affects any version with the stream iterator helpers, i.e. Readable.find/some/every) Platform Linux (reproduces regardless of platform - pure JS logic issue) Subsystem No response What...
Open Graph Description: Version v24.18.0 (reproduces on this version; likely affects any version with the stream iterator helpers, i.e. Readable.find/some/every) Platform Linux (reproduces regardless of platform - pure JS...
X Description: Version v24.18.0 (reproduces on this version; likely affects any version with the stream iterator helpers, i.e. Readable.find/some/every) Platform Linux (reproduces regardless of platform - pure JS...
Opengraph URL: https://github.com/nodejs/node/issues/64261
X: @github
Domain: github.com
{"@context":"https://schema.org","@type":"DiscussionForumPosting","headline":"`readable.find()`/`some()`/`every()` don't destroy the stream on match when the source hasn't ended","articleBody":"### Version\n\nv24.18.0 (reproduces on this version; likely affects any version with the stream iterator helpers, i.e. Readable.find/some/every)\n\n### Platform\n\nLinux (reproduces regardless of platform - pure JS logic issue)\n\n### Subsystem\n\n_No response_\n\n### What steps will reproduce the bug?\n\n```js\nimport { PassThrough } from \"node:stream\";\n\nconst pt = new PassThrough();\n\nconst matchPromise = pt.find((chunk) => chunk.toString().includes(\"MATCH\"));\n\npt.write(\"first line\\n\");\npt.write(\"MATCH this one\\n\");\n\nconst result = await matchPromise;\nconsole.log(\"find() resolved with:\", JSON.stringify(result.toString()));\nconsole.log(\"pt.destroyed immediately after resolution:\", pt.destroyed);\n\nawait new Promise((r) => setTimeout(r, 100));\nconsole.log(\"pt.destroyed 100ms later:\", pt.destroyed);\n```\n\n### How often does it reproduce? Is there a required condition?\n\n100% reproducible. Condition: the source stream must still be open (not `.end()`-ed) and must not receive another chunk immediately after the matching one. This is the common case for any long-lived/never-ending stream (e.g. tailing a child process's stdout/stderr, or any other indefinite source) - which is exactly the kind of stream `.find()`/`.some()`/`.every()` are attractive for using with (`for await`-based line/event scanning).\n\n### What is the expected behavior? Why is that the expected behavior?\n\nPer the documentation for [`readable.find(fn[, options])`](https://nodejs.org/api/stream.html#readablefindfn-options) (identical wording for `.some()` / `.every()`):\n\n> Once an `fn` call's awaited return value is truthy, **the stream is destroyed** and the promise is fulfilled with value for which `fn` returned a truthy value.\n\nSo `pt.destroyed` should become `true` once `find()`'s promise resolves (or shortly after, since `.destroy()` is not fully synchronous).\n\n### What do you see instead?\n\n```\nfind() resolved with: \"first line\\nMATCH this one\\n\"\npt.destroyed immediately after resolution: false\npt.destroyed 100ms later: false\n```\n\n`pt` is never destroyed, no matter how long you wait, as long as nothing else writes another chunk to it (in this repro, it's just never destroyed at all, period, since nothing writes again).\n\n### Additional information\n\nThe root cause, traced in `lib/internal/streams/operators.js`:\n\n`find` is a thin wrapper around `filter`, which is a thin wrapper around `map`:\n\n```js\nasync function find(fn, options) {\n for await (const result of filter.call(this, fn, options)) {\n return result;\n }\n return undefined;\n}\n```\n\nThe `return result` triggers the implicit `.return()` call on the `filter`/`map` async generator (standard `for await...of` early-exit semantics) - this correctly unwinds the OUTER generator's own `finally` block (setting `done = true` and resolving pending `resume`/`next` promises inside `map`).\n\nHowever, `map`'s implementation spawns a *separate, fire-and-forget* `pump()` async function that independently iterates the original source stream:\n\n```js\nasync function pump() {\n try {\n for await (let val of stream) { // \u003c-- this is a DIFFERENT for-await-of loop, over the original source\n if (done) {\n return;\n }\n ...\n```\n\n`pump()`'s `for await (let val of stream)` is a *different* iterator/loop than the one `find()`'s `for await` unwinds. Setting `done = true` from the outer generator's cleanup does not itself unblock this inner loop - it's only checked *after* `stream`'s iterator yields a new value. If the source never produces another chunk after the matching one (because it's still open and simply idle), `pump()` stays permanently suspended awaiting the next chunk from `stream`, and nothing ever calls `.destroy()` (or `.return()`) on `stream` itself.\n\nThere is no explicit `stream.destroy()` call anywhere in `map`/`filter`/`find`'s implementation in `lib/internal/streams/operators.js` - the \"stream is destroyed\" behavior described in the docs appears to rely entirely on the source stream separately reaching natural end/auto-destroy on its own, which only coincidentally happens to align with \"matched\" for finite/short-lived sources, not because `find`/`some`/`every` themselves ever destroy it.\n\n(I confirmed this distinction experimentally: a finite `Readable.from([...])` source does eventually show `destroyed: true` after a match, but that's because the array-backed stream naturally runs to completion regardless of the match and gets auto-destroyed via `autoDestroy` once it truly ends - not because `find()` destroyed it. A still-open, indefinite source like the `PassThrough` above never gets destroyed at all.)\n\nAnyone relying on the documented \"the stream is destroyed\" behavior to avoid manually cleaning up resources after `find()`/`some()`/`every()` on a long-lived stream (e.g. scanning a child process's stdout for a marker line) will leak that stream indefinitely - it stays alive, un-destroyed, holding onto internal buffers/decoders, until whatever holds a reference to it is garbage collected, with no explicit teardown ever occurring.","author":{"url":"https://github.com/perrin4869","@type":"Person","name":"perrin4869"},"datePublished":"2026-07-02T15:33:54Z","url":"https://github.com/nodejs/node/issues/64261"}
| 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:26bc448e-52b3-de8e-57c4-19fec14ac69f |
| current-catalog-service-hash | 81bb79d38c15960b92d99bca9288a9108c7a47b18f2423d0f6438c5b7bcd2114 |
| request-id | 83AA:20B00F:1E4BE1:29800A:6AED4364 |
| html-safe-nonce | cf77dc41dc3a32fc4678731f0359c130e69533acda9febf8345ef2bba742a1ff |
| visitor-payload | eyJyZWZlcnJlciI6IiIsInJlcXVlc3RfaWQiOiI4M0FBOjIwQjAwRjoxRTRCRTE6Mjk4MDBBOjZBRUQ0MzY0IiwidmlzaXRvcl9pZCI6IjUxODg1NzI1MTk3NDc4OTYxNjQiLCJyZWdpb25fZWRnZSI6ImlhZCIsInJlZ2lvbl9yZW5kZXIiOiJpYWQifQ== |
| visitor-hmac | 5b2a7315ac874ca1939644b53d5dd05d743e2d06fa2a278cef321d64392416e4 |
| hovercard-subject-tag | issue:4795927556 |
| 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/64261/issue_layout |
| twitter:image | https://opengraph.githubassets.com/5156e4a7664156e792eedd616e65ad6255a84700ab4fac762d28e066d40df338/nodejs/node/issues/64261 |
| twitter:card | summary_large_image |
| og:image | https://opengraph.githubassets.com/5156e4a7664156e792eedd616e65ad6255a84700ab4fac762d28e066d40df338/nodejs/node/issues/64261 |
| og:image:alt | Version v24.18.0 (reproduces on this version; likely affects any version with the stream iterator helpers, i.e. Readable.find/some/every) Platform Linux (reproduces regardless of platform - pure JS... |
| og:image:width | 1200 |
| og:image:height | 600 |
| og:site_name | GitHub |
| og:type | object |
| og:author:username | perrin4869 |
| hostname | github.com |
| expected-hostname | github.com |
| None | 56fb42677ae87ed3e64410323d67980aa65d0485568a7858f874e7edfc6d1c11 |
| 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 | 9c9a37e2423ed382cf39f70b5aa4b4858b40a3b0 |
| ui-target | full |
| theme-color | #1e2327 |
| color-scheme | light dark |
| react-app-name | issues |
Links:
Viewport: width=device-width