Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions doc/api/diagnostics_channel.md
Original file line number Diff line number Diff line change
Expand Up @@ -831,14 +831,15 @@ added:
* `context` {Object} Shared object to correlate trace events through
* `thisArg` {any} The receiver to be used for the function call
* `...args` {any} Optional arguments to pass to the function
* Returns: {Promise} Chained from promise returned by the given function
* Returns: {any} The return value of the given function

Trace a promise-returning function call. This will always produce a
[`start` event][] and [`end` event][] around the synchronous portion of the
function execution, and will produce an [`asyncStart` event][] and
[`asyncEnd` event][] when a promise continuation is reached. It may also
produce an [`error` event][] if the given function throws an error or the
returned promise rejects. This will run the given function using
function execution. If the return value is a {Promise} or [thenable][], it will
also produce an [`asyncStart` event][] and [`asyncEnd` event][] when the
returned promise is fulfilled or rejected. It may also produce an
[`error` event][] if the given function throws an error or the returned promise
rejects. This will run the given function using
[`channel.runStores(context, ...)`][] on the `start` channel which ensures all
events should have any bound stores set to match this trace context.

Expand Down Expand Up @@ -1436,3 +1437,4 @@ Emitted when a new thread is created.
[`process.execve()`]: process.md#processexecvefile-args-env
[`start` event]: #startevent
[context loss]: async_context.md#troubleshooting-context-loss
[thenable]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise#thenables
22 changes: 13 additions & 9 deletions lib/diagnostics_channel.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@ const {
ObjectDefineProperty,
ObjectGetPrototypeOf,
ObjectSetPrototypeOf,
Promise,
PromisePrototypeThen,
PromiseReject,
PromiseResolve,
ReflectApply,
SafeFinalizationRegistry,
Expand All @@ -25,6 +23,9 @@ const {
ERR_INVALID_ARG_TYPE,
},
} = require('internal/errors');
const {
isPromise,
} = require('internal/util/types');
const {
validateFunction,
} = require('internal/validators');
Expand Down Expand Up @@ -358,25 +359,28 @@ class TracingChannel {
asyncStart.publish(context);
// TODO: Is there a way to have asyncEnd _after_ the continuation?
asyncEnd.publish(context);
return PromiseReject(err);
}

function resolve(result) {
context.result = result;
asyncStart.publish(context);
// TODO: Is there a way to have asyncEnd _after_ the continuation?
asyncEnd.publish(context);
return result;
}

return start.runStores(context, () => {
try {
let promise = ReflectApply(fn, thisArg, args);
// Convert thenables to native promises
if (!(promise instanceof Promise)) {
promise = PromiseResolve(promise);
const result = ReflectApply(fn, thisArg, args);
// If the result isn't a Promise or thenable, then make this the
// trace result immediately.
// Otherwise, register the async tracing callbacks, then return the
// original promise.
if (!isPromise(result) && typeof result?.then !== 'function') {
context.result = result;
} else {
PromisePrototypeThen(PromiseResolve(result), resolve, reject);
Copy link
Member

Choose a reason for hiding this comment

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

doesn't this swallow a reject in case the caller never registers a than nor a catch handler?

Copy link
Contributor Author

@Renegade334 Renegade334 Sep 20, 2025

Choose a reason for hiding this comment

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

It does not. The returned promise isn't the one returned by .then(), but the original promise, which remains rejected regardless of whether any reject callbacks have been called.

The promise returned by .then() would indeed never reject, but this is just discarded.

Copy link
Member

@Flarna Flarna Sep 21, 2025

Choose a reason for hiding this comment

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

Did a fast try. Following test is green now but fails with your change:

'use strict';

const common = require('../common');
const dc = require('diagnostics_channel');
const assert = require('assert');

const channel = dc.tracingChannel('test');

const expectedError = new Error('test');
const input = { foo: 'bar' };
const thisArg = { baz: 'buz' };

process.on('unhandledRejection', common.mustCall((reason) => {
  assert.deepStrictEqual(reason, expectedError);
}));

function check(found) {
  assert.deepStrictEqual(found, input);
}

const handlers = {
  start: common.mustCall(check),
  end: common.mustCall(check),
  asyncStart: common.mustCall(check),
  asyncEnd: common.mustCall(check),
  error: common.mustCall((found) => {
    check(found);
    assert.deepStrictEqual(found.error, expectedError);
  })
};

channel.subscribe(handlers);

// Set no then/catch handler to verify unhandledRejection event
channel.tracePromise(function(value) {
  assert.deepStrictEqual(this, thisArg);
  return Promise.reject(value);
}, input, thisArg, expectedError);

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Unhandled rejection errors would indeed be suppressed by this approach.

}
return PromisePrototypeThen(promise, resolve, reject);
return result;
} catch (err) {
context.error = err;
error.publish(context);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ AssertionError [ERR_ASSERTION]: The expression evaluated to a falsy value:
*
*
*
at TracingChannel.traceSync (node:diagnostics_channel:328:14)
at TracingChannel.traceSync (node:diagnostics_channel:329:14)
*
*
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
'use strict';

const common = require('../common');
const dc = require('diagnostics_channel');
const assert = require('assert');

const kSuccess = Symbol('kSuccess');
const kFailure = Symbol('kFailure');

class DerivedPromise extends Promise {}

const fulfilledThenable = {
then(resolve) {
resolve(kSuccess);
return this;
},
};
const rejectedThenable = {
then(_, reject) {
reject(kFailure);
return this;
},
};

function check(event) {
if (event.expectError) {
assert.strictEqual(event.error, kFailure);
assert.strictEqual(event.result, undefined);
} else {
assert.strictEqual(event.error, undefined);
assert.strictEqual(event.result, kSuccess);
}
}

let i = 0;

function test(expected, { withSubscribers, expectAsync, expectError }) {
const channel = dc.tracingChannel(`test${i++}`);

if (withSubscribers) {
channel.subscribe({
start: common.mustCall(),
end: common.mustCall(),
asyncStart: expectAsync ? common.mustCall(check) : common.mustNotCall(),
asyncEnd: expectAsync ? common.mustCall(check) : common.mustNotCall(),
error: expectError ? common.mustCall(check) : common.mustNotCall(),
});
}

const result = channel.tracePromise(() => expected, { expectError });
assert.strictEqual(result, expected);
}

// Should trace Promises and thenables, and return them unaltered
const testValues = [
[DerivedPromise.resolve(kSuccess), false],
[DerivedPromise.reject(kFailure), true],
[fulfilledThenable, false],
[rejectedThenable, true],
];
for (const [value, expectError] of testValues) {
test(value, { withSubscribers: false });
test(value, { withSubscribers: true, expectAsync: true, expectError });
}

// Should return non-thenable values unaltered, and should not call async handlers
test(kSuccess, { withSubscribers: false });
test(kSuccess, { withSubscribers: true, expectAsync: false, expectError: false });

// Should throw synchronous exceptions unaltered
{
const channel = dc.tracingChannel(`test${i++}`);

const error = new Error('Synchronous exception');
const throwError = () => { throw error; };

assert.throws(() => channel.tracePromise(throwError), error);

channel.subscribe({
start: common.mustCall(),
end: common.mustCall(),
asyncStart: common.mustNotCall(),
asyncEnd: common.mustNotCall(),
error: common.mustCall(),
});

assert.throws(() => channel.tracePromise(throwError), error);
}
Loading