Skip to content
Open
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
32 changes: 23 additions & 9 deletions doc/api/diagnostics_channel.md
Original file line number Diff line number Diff line change
Expand Up @@ -829,23 +829,36 @@ channels.traceSync(() => {
added:
- v19.9.0
- v18.19.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/61766
description: Custom thenables will no longer be wrapped in native Promises.
Non-thenables will be returned with a warning.
-->

* `fn` {Function} Promise-returning function to wrap a trace around
* `fn` {Function} Function to wrap a trace around
* `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

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
* Returns: {any} The return value of the given function, or the result of
calling `.then(...)` on the return value if the tracing channel has active
subscribers. If the return value is not a Promise or thenable, then
it is returned as-is and a warning is emitted.

Trace an asynchronous function call which returns a {Promise} or
[thenable object][]. 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 the
returned promise is resolved or rejected. It may also produce an
[`error` event][] if the given function throws an error or the returned promise
is rejected. 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.

If the value returned by `fn` is not a Promise or thenable, then it will be
returned with a warning, and no `asyncStart` or `asyncEnd` events will be
produced.

To ensure only correct trace graphs are formed, events will only be published
if subscribers are present prior to starting the trace. Subscriptions which are
added after the trace begins will not receive future events from that trace,
Expand Down Expand Up @@ -1457,3 +1470,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 object]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise#thenables
30 changes: 19 additions & 11 deletions lib/diagnostics_channel.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,6 @@ const {
ObjectDefineProperty,
ObjectGetPrototypeOf,
ObjectSetPrototypeOf,
Promise,
PromisePrototypeThen,
PromiseReject,
PromiseResolve,
ReflectApply,
SafeFinalizationRegistry,
SafeMap,
Expand Down Expand Up @@ -280,6 +276,11 @@ function tracingChannelFrom(nameOrChannels, name) {
nameOrChannels);
}

function emitNonThenableWarning(fn) {
process.emitWarning(`tracePromise was called with the function '${fn.name || '<anonymous>'}', ` +
'which returned a non-thenable.');
}

class TracingChannel {
constructor(nameOrChannels) {
for (let i = 0; i < traceEvents.length; ++i) {
Expand Down Expand Up @@ -347,7 +348,11 @@ class TracingChannel {

tracePromise(fn, context = {}, thisArg, ...args) {
if (!this.hasSubscribers) {
return ReflectApply(fn, thisArg, args);
const result = ReflectApply(fn, thisArg, args);
if (typeof result.then !== 'function') {
emitNonThenableWarning(fn);
}
return result;
}

const { start, end, asyncStart, asyncEnd, error } = this;
Expand All @@ -358,7 +363,7 @@ class TracingChannel {
asyncStart.publish(context);
// TODO: Is there a way to have asyncEnd _after_ the continuation?
asyncEnd.publish(context);
return PromiseReject(err);
throw err;
Copy link
Member Author

Choose a reason for hiding this comment

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

Throwing here avoids Promisifying thenables on rejection. The Promises/A+ standard dictates that throwing within the promise handlers results in the returned thenable rejecting. (https://promisesaplus.com/#point-42)

For native Promises, there should be no observable change.

}

function resolve(result) {
Expand All @@ -371,12 +376,15 @@ class TracingChannel {

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 return value is not a thenable, then return it with a warning.
// Do not publish to asyncStart/asyncEnd.
if (typeof result.then !== 'function') {
emitNonThenableWarning(fn);
context.result = result;
return result;
}
return PromisePrototypeThen(promise, resolve, reject);
return result.then(resolve, reject);
} catch (err) {
context.error = err;
error.publish(context);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
'use strict';

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

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

const expectedResult = { foo: 'bar' };
const input = { foo: 'bar' };
const thisArg = { baz: 'buz' };

function checkStart(found) {
assert.strictEqual(found, input);
}

function checkEnd(found) {
checkStart(found);
assert.strictEqual(found.error, undefined);
assert.deepStrictEqual(found.result, expectedResult);
}

const handlers = {
start: common.mustCall(checkStart),
end: common.mustCall(checkEnd),
asyncStart: common.mustNotCall(),
asyncEnd: common.mustNotCall(),
error: common.mustNotCall()
};

channel.subscribe(handlers);

process.on('warning', common.mustCall((warning) => {
assert.strictEqual(
warning.message,
"tracePromise was called with the function '<anonymous>', which returned a non-thenable."
);
}));

assert.strictEqual(
channel.tracePromise(common.mustCall(function(value) {
assert.deepStrictEqual(this, thisArg);
return value;
}), input, thisArg, expectedResult),
expectedResult,
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
'use strict';

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

class ResolvedThenable {
#result;
constructor(value) {
this.#result = value;
}
then(resolve) {
return new ResolvedThenable(resolve(this.#result));
}
}

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

const expectedResult = { foo: 'bar' };
const input = { foo: 'bar' };
const thisArg = { baz: 'buz' };

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

function checkAsync(found) {
check(found);
assert.strictEqual(found.error, undefined);
assert.deepStrictEqual(found.result, expectedResult);
}

const handlers = {
start: common.mustCall(check),
end: common.mustCall(check),
asyncStart: common.mustCall(checkAsync),
asyncEnd: common.mustCall(checkAsync),
error: common.mustNotCall()
};

channel.subscribe(handlers);

let innerThenable;

const result = channel.tracePromise(common.mustCall(function(value) {
assert.deepStrictEqual(this, thisArg);
innerThenable = new ResolvedThenable(value);
return innerThenable;
}), input, thisArg, expectedResult);

assert(result instanceof ResolvedThenable);
assert.notStrictEqual(result, innerThenable);
result.then(common.mustCall((value) => {
assert.deepStrictEqual(value, expectedResult);
}));
Loading