Title: Eagerly format default AbortController abort reasons to avoid retaining canceled work · Issue #66192 · nodejs/node · GitHub
Open Graph Title: Eagerly format default AbortController abort reasons to avoid retaining canceled work · Issue #66192 · nodejs/node
X Title: Eagerly format default AbortController abort reasons to avoid retaining canceled work · Issue #66192 · nodejs/node
Description: Could controller.abort() eagerly read the default reason’s stack, following the existing streams workaround and its original discussion? A retained signal can keep the canceled job alive through signal → reason → V8 stack → caller’s rece...
Open Graph Description: Could controller.abort() eagerly read the default reason’s stack, following the existing streams workaround and its original discussion? A retained signal can keep the canceled job alive through si...
X Description: Could controller.abort() eagerly read the default reason’s stack, following the existing streams workaround and its original discussion? A retained signal can keep the canceled job alive through si...
Opengraph URL: https://github.com/nodejs/node/issues/66192
X: @github
Domain: github.com
{"@context":"https://schema.org","@type":"DiscussionForumPosting","headline":"Eagerly format default AbortController abort reasons to avoid retaining canceled work","articleBody":"Could `controller.abort()` eagerly read the default reason’s stack, following the [existing streams workaround](https://github.com/nodejs/node/blob/v26.8.2/lib/internal/streams/destroy.js#L31-L35) and its [original discussion](https://github.com/nodejs/node/pull/34103#issuecomment-652002364)?\n\nA retained signal can keep the canceled job alive through `signal → reason → V8 stack → caller’s receiver`, including large buffers. This surfaced while investigating [jsdom window retention](https://github.com/jsdom/jsdom/issues/4337).\n\nRun this as an `.mjs` file with `node --expose-gc`. Verified on Node **v26.8.2**, Linux x64:\n\n```js\nimport { setImmediate } from \"node:timers/promises\";\n\nclass RenderJob {\n input = Buffer.alloc(32 * 1024 * 1024, 1);\n controller = new AbortController();\n\n cancel() {\n this.controller.abort();\n return this.controller.signal;\n }\n}\n\nasync function retainedMiB() {\n for (let i = 0; i \u003c 5; ++i) {\n await setImmediate();\n global.gc();\n }\n return Math.round(process.memoryUsage().arrayBuffers / 1024 ** 2);\n}\n\n// Model dependencies retaining signals after the jobs are discarded.\nconst signals = Array.from({ length: 4 }, () => new RenderJob().cancel());\nconsole.log(await retainedMiB()); // 128\nconsole.log(signals.every(signal => signal.aborted)); // true\n\nfor (const signal of signals) void signal.reason.stack;\n\nconsole.log(await retainedMiB()); // 0\nconsole.log(signals.every(signal => signal.aborted)); // true\n```\n\nThe same signals and reasons remain alive throughout; reading their stacks releases all 128 MiB.\n\nI propose formatting only newly created default reasons, leaving caller-supplied reasons untouched. This preserves error identity and stack text with the default formatter. Formatting should follow abort notifications, and formatter exceptions should not escape `abort()`.\n\n\u003cdetails>\n\u003csummary>GPT-6 Astra Extra High's suggested diff\u003c/summary>\n\n```diff\n- abort(reason = new DOMException('This operation was aborted', 'AbortError')) {\n- abortSignal(this.#signal ??= new AbortSignal(kDontThrowSymbol), reason);\n+ abort(reason = undefined) {\n+ const signal = this.#signal ??= new AbortSignal(kDontThrowSymbol);\n+ if (signal[kAborted]) return;\n+\n+ const isDefaultReason = reason === undefined;\n+ if (isDefaultReason) {\n+ reason = new DOMException('This operation was aborted', 'AbortError');\n+ }\n+\n+ abortSignal(signal, reason);\n+\n+ if (isDefaultReason) {\n+ try {\n+ // Release references retained by V8's unformatted stack.\n+ // See https://github.com/nodejs/node/pull/34103#issuecomment-652002364\n+ reason.stack; // eslint-disable-line no-unused-expressions\n+ } catch {\n+ // A custom stack formatter must not make cancellation throw.\n+ }\n+ }\n }\n```\n\n\u003c/details>\n\nThe tradeoffs are extra formatting work and earlier `Error.prepareStackTrace` execution. Custom formatters that retain call sites or throw may still retain the objects. This is the same compromise streams already makes.\n\nAlternately, if the Node.js team has good V8 contacts, it might be good to raise this with them. It's kind of ridiculous for large objects to be retained in this way all because of `Error.prepareStackTrace`, in my opinion.","author":{"url":"https://github.com/domenic","@type":"Person","name":"domenic"},"datePublished":"2026-09-22T01:21:57Z","url":"https://github.com/nodejs/node/issues/66192"}
| 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:2f59b6b3-a086-7a0c-d4d6-a69c7037e519 |
| current-catalog-service-hash | 81bb79d38c15960b92d99bca9288a9108c7a47b18f2423d0f6438c5b7bcd2114 |
| request-id | E9EE:2984B8:A4BEF:E1E5F:6AED333B |
| html-safe-nonce | 6dd0932a2f88a2f41fd3372de1a6899fbf40233a6e9eb7298996e76a4bcbce44 |
| visitor-payload | eyJyZWZlcnJlciI6IiIsInJlcXVlc3RfaWQiOiJFOUVFOjI5ODRCODpBNEJFRjpFMUU1Rjo2QUVEMzMzQiIsInZpc2l0b3JfaWQiOiI2MTEzMzIxNjQ0NzgxODEwNDkyIiwicmVnaW9uX2VkZ2UiOiJpYWQiLCJyZWdpb25fcmVuZGVyIjoiaWFkIn0= |
| visitor-hmac | 8c66369ffcafeec36302a528b561899a91b77e7c4a399bcd6a12ee0dfc994dca |
| hovercard-subject-tag | issue:5534208906 |
| 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/66192/issue_layout |
| twitter:image | https://opengraph.githubassets.com/78aac231e99f5077fe2fa816d6f473e38ccc7b8154a0ab66ee0befa097771ab3/nodejs/node/issues/66192 |
| twitter:card | summary_large_image |
| og:image | https://opengraph.githubassets.com/78aac231e99f5077fe2fa816d6f473e38ccc7b8154a0ab66ee0befa097771ab3/nodejs/node/issues/66192 |
| og:image:alt | Could controller.abort() eagerly read the default reason’s stack, following the existing streams workaround and its original discussion? A retained signal can keep the canceled job alive through si... |
| og:image:width | 1200 |
| og:image:height | 600 |
| og:site_name | GitHub |
| og:type | object |
| og:author:username | domenic |
| hostname | github.com |
| expected-hostname | github.com |
| None | e51d0b7b5ab34b1688c05b54c3ed17b8658b2fa9bca973a9c740683ccb12cf4a |
| 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 | 59ebb7b901cc154ecc179a44f266fcf69f343b30 |
| ui-target | full |
| theme-color | #1e2327 |
| color-scheme | light dark |
| react-app-name | issues |
Links:
Viewport: width=device-width