Skip to content

diagnostics_channel: replace using with try-finally#64251

Open
ayush23chaudhary wants to merge 1 commit into
nodejs:mainfrom
ayush23chaudhary:fix-erm-diagnostics
Open

diagnostics_channel: replace using with try-finally#64251
ayush23chaudhary wants to merge 1 commit into
nodejs:mainfrom
ayush23chaudhary:fix-erm-diagnostics

Conversation

@ayush23chaudhary

Copy link
Copy Markdown

Fixes: #64230

Description

This PR replaces instances of the experimental using syntax with try...finally blocks inside lib/diagnostics_channel.js.

Since Explicit Resource Management is still a flagged feature in V8, encountering using in core causes a segfault when running Node with the --no-js-explicit-resource-management flag. This refactor maintains the exact same resource cleanup semantics by manually calling [SymbolDispose]() in a finally block, avoiding the flagged syntax entirely.

Example of Refactor

Before:

using scope = this.withStoreScope(data);
return ReflectApply(fn, thisArg, args);

After

const scope = this.withStoreScope(data);
try {
  return ReflectApply(fn, thisArg, args);
} finally {
  scope[SymbolDispose]();
}

Copilot AI review requested due to automatic review settings July 2, 2026 04:13
@nodejs-github-bot nodejs-github-bot added diagnostics_channel Issues and PRs related to diagnostics channel needs-ci PRs that need a full CI run. labels Jul 2, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR removes the experimental using syntax from lib/diagnostics_channel.js, replacing it with equivalent try...finally disposal to avoid crashes when Node is run with --no-js-explicit-resource-management.

Changes:

  • Replaced using statements with explicit try...finally blocks that invoke [SymbolDispose]().
  • Updated scope handling in ActiveChannel, BoundedChannel, and TracingChannel paths to ensure disposal occurs reliably without relying on flagged syntax.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread lib/diagnostics_channel.js
Comment thread lib/diagnostics_channel.js
Comment thread lib/diagnostics_channel.js
Comment thread lib/diagnostics_channel.js Outdated
triggerUncaughtException(err, false);
});
continue;
const stack = new DisposableStack();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DisposableStack is also a flagged global.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Renegade334, I did not realize DisposableStack was part of the same flagged feature. I've replaced it with a standard array that manually iterates in reverse during disposal. Just pushed the update.

@anonrig anonrig left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add at least a test

Comment thread lib/diagnostics_channel.js Outdated
Comment on lines +120 to +126
} finally {
if (this.#stack === undefined) {
for (let i = stack.length - 1; i >= 0; i--) {
stack[i][SymbolDispose]();
}
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#stack is not disposed by the constructor. This code should be removed, and the try ... finally block in the constructor is not needed.

Comment thread lib/diagnostics_channel.js
Comment thread lib/diagnostics_channel.js Outdated
Comment on lines +131 to +134
for (let i = this.#stack.length - 1; i >= 0; i--) {
this.#stack[i][SymbolDispose]();
}
this.#stack = undefined;

@Renegade334 Renegade334 Jul 2, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
for (let i = this.#stack.length - 1; i >= 0; i--) {
this.#stack[i][SymbolDispose]();
}
this.#stack = undefined;
const stack = this.#stack;
this.#stack = undefined;
for (let i = stack.length - 1; i >= 0; i--) {
stack[i][SymbolDispose]();
}

Matches the order of operations of DisposableStack.prototype.dispose(), in case anything touches the stack during disposal.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can any of the store disposers ever throw? If so, we need error handling here as well.

@ayush23chaudhary

ayush23chaudhary commented Jul 2, 2026

Copy link
Copy Markdown
Author

Thanks for the thorough review, @Renegade334 and @anonrig!

  1. I've updated RunStoresScope to perfectly match the suggested error handling and disposal order (NodeAggregateError).

  2. I've added a test in test/parallel/test-diagnostics-channel-no-erm.js to ensure Node does not segfault when run with --no-js-explicit-resource-management.

  3. I squashed everything into a single commit to resolve the commit linter failure.

@Renegade334 Renegade334 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your changes! Some more comments 👍

Comment on lines +553 to +563
try {
const result = ReflectApply(fn, thisArg, args);
context.result = result;
return result;
} catch (err) {
context.error = err;
error.publish(context);
throw err;
}
} finally {
scope[SymbolDispose]();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no need to nest this, it can just be try ... catch ... finally.

Suggested change
try {
const result = ReflectApply(fn, thisArg, args);
context.result = result;
return result;
} catch (err) {
context.error = err;
error.publish(context);
throw err;
}
} finally {
scope[SymbolDispose]();
const result = ReflectApply(fn, thisArg, args);
context.result = result;
return result;
} catch (err) {
context.error = err;
error.publish(context);
throw err;
} finally {
scope[SymbolDispose]();

Comment on lines +584 to +588
try {
// TODO: Is there a way to have asyncEnd _after_ the continuation?
} finally {
scope[SymbolDispose]();
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need for an empty block here.

Suggested change
try {
// TODO: Is there a way to have asyncEnd _after_ the continuation?
} finally {
scope[SymbolDispose]();
}
// TODO: Is there a way to have asyncEnd _after_ the continuation?
scope[SymbolDispose]();

Comment on lines +610 to 634
try {
const result = ReflectApply(fn, thisArg, args);
// If the return value is not a thenable, return it directly with a warning.
// Do not publish to asyncStart/asyncEnd.
if (typeof result?.then !== 'function') {
emitNonThenableWarning(fn);
context.result = result;
return result;
}
// isPromise() matches sub-classes, but we need to match only direct
// instances of the native Promise type to safely use PromisePrototypeThen.
if (isPromise(result) && ObjectGetPrototypeOf(result) === PromisePrototype) {
return PromisePrototypeThen(result, onResolve, onRejectWithRethrow);
}
// For non-native thenables, subscribe to the result but return the
// original thenable so the consumer can continue handling it directly.
// Non-native thenables don't have unhandledRejection tracking, so
// swallowing the rejection here doesn't change existing behaviour.
result.then(onResolve, onReject);
return result;
} catch (err) {
context.error = err;
error.publish(context);
throw err;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here.

Suggested change
try {
const result = ReflectApply(fn, thisArg, args);
// If the return value is not a thenable, return it directly with a warning.
// Do not publish to asyncStart/asyncEnd.
if (typeof result?.then !== 'function') {
emitNonThenableWarning(fn);
context.result = result;
return result;
}
// isPromise() matches sub-classes, but we need to match only direct
// instances of the native Promise type to safely use PromisePrototypeThen.
if (isPromise(result) && ObjectGetPrototypeOf(result) === PromisePrototype) {
return PromisePrototypeThen(result, onResolve, onRejectWithRethrow);
}
// For non-native thenables, subscribe to the result but return the
// original thenable so the consumer can continue handling it directly.
// Non-native thenables don't have unhandledRejection tracking, so
// swallowing the rejection here doesn't change existing behaviour.
result.then(onResolve, onReject);
return result;
} catch (err) {
context.error = err;
error.publish(context);
throw err;
}
const result = ReflectApply(fn, thisArg, args);
// If the return value is not a thenable, return it directly with a warning.
// Do not publish to asyncStart/asyncEnd.
if (typeof result?.then !== 'function') {
emitNonThenableWarning(fn);
context.result = result;
return result;
}
// isPromise() matches sub-classes, but we need to match only direct
// instances of the native Promise type to safely use PromisePrototypeThen.
if (isPromise(result) && ObjectGetPrototypeOf(result) === PromisePrototype) {
return PromisePrototypeThen(result, onResolve, onRejectWithRethrow);
}
// For non-native thenables, subscribe to the result but return the
// original thenable so the consumer can continue handling it directly.
// Non-native thenables don't have unhandledRejection tracking, so
// swallowing the rejection here doesn't change existing behaviour.
result.then(onResolve, onReject);
return result;
} catch (err) {
context.error = err;
error.publish(context);
throw err;

Comment on lines +671 to +679
try {
return ReflectApply(fn, thisArg, args);
} catch (err) {
context.error = err;
error.publish(context);
throw err;
}
} finally {
scope[SymbolDispose]();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here.

Suggested change
try {
return ReflectApply(fn, thisArg, args);
} catch (err) {
context.error = err;
error.publish(context);
throw err;
}
} finally {
scope[SymbolDispose]();
return ReflectApply(fn, thisArg, args);
} catch (err) {
context.error = err;
error.publish(context);
throw err;
} finally {
scope[SymbolDispose]();

Comment on lines +10 to +14
const child = spawnSync(process.execPath, [
'--no-js-explicit-resource-management',
'--eval',
'require("diagnostics_channel").tracingChannel("foo").traceSync(() => {})'
]);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test runner can handle this abstraction for you. Just place a line like the following at the top of the script:

// Flags: --no-js-explicit-resource-management

and the test script will be run under those runtime flags, without needing to spawn a new process. See https://github.com/nodejs/node/blob/main/test/parallel/test-permission-has.js for an example.

Comment on lines +129 to +145
const errors = [];
for (let i = stack.length - 1; i >= 0; i--) {
try {
stack[i][SymbolDispose]();
} catch (error) {
ArrayPrototypePush(errors, error);
}
}

if (errors.length === 0) {
return;
} else if (errors.length === 1) {
throw errors[0];
} else {
throw new NodeAggregateError(errors);
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've had a look, and all of the scope disposers should never throw synchronously (they wrap their code in their own try ... catch blocks and emit errors asynchronously). As such, I don't think we need to implement this, and we can use your original version without the error handling.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

diagnostics_channel Issues and PRs related to diagnostics channel needs-ci PRs that need a full CI run.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Segfaults with [await] using within node core

5 participants