From 22418504124ac7854870b928d0292c7349ad9293 Mon Sep 17 00:00:00 2001 From: Joyee Cheung Date: Mon, 15 Dec 2025 21:37:06 +0100 Subject: [PATCH 1/2] async_hooks: add trackPromises option to createHook() This adds a trackPromises option that allows users to completely opt out of the promise hooks that are installed whenever an async hook is added. For those who do not need to track promises, this avoids the excessive hook invocation and the heavy overhead from it. This option was previously already implemented internally to skip the noise from promise hooks when debugging async operations via the V8 inspector. This patch just exposes it. --- doc/api/async_hooks.md | 43 +++++++++++++++++-- lib/async_hooks.js | 18 +++++++- lib/internal/inspector_async_hook.js | 3 +- .../test-track-promises-default.js | 16 +++++++ .../test-track-promises-false-check.js | 19 ++++++++ test/async-hooks/test-track-promises-false.js | 11 +++++ test/async-hooks/test-track-promises-true.js | 17 ++++++++ .../test-track-promises-validation.js | 25 +++++++++++ 8 files changed, 144 insertions(+), 8 deletions(-) create mode 100644 test/async-hooks/test-track-promises-default.js create mode 100644 test/async-hooks/test-track-promises-false-check.js create mode 100644 test/async-hooks/test-track-promises-false.js create mode 100644 test/async-hooks/test-track-promises-true.js create mode 100644 test/async-hooks/test-track-promises-validation.js diff --git a/doc/api/async_hooks.md b/doc/api/async_hooks.md index 07793fca90e445..a863350fc2d26d 100644 --- a/doc/api/async_hooks.md +++ b/doc/api/async_hooks.md @@ -144,18 +144,20 @@ function destroy(asyncId) { } function promiseResolve(asyncId) { } ``` -## `async_hooks.createHook(callbacks)` +## `async_hooks.createHook(options)` -* `callbacks` {Object} The [Hook Callbacks][] to register +* `options` {Object} The [Hook Callbacks][] to register * `init` {Function} The [`init` callback][]. * `before` {Function} The [`before` callback][]. * `after` {Function} The [`after` callback][]. * `destroy` {Function} The [`destroy` callback][]. * `promiseResolve` {Function} The [`promiseResolve` callback][]. + * `trackPromises` {boolean} Whether the hook should track `Promise`s. Cannot be `false` if + `promiseResolve` is set. **Default**: `true`. * Returns: {AsyncHook} Instance used for disabling and enabling hooks Registers functions to be called for different lifetime events of each async @@ -354,7 +356,8 @@ Furthermore users of [`AsyncResource`][] create async resources independent of Node.js itself. There is also the `PROMISE` resource type, which is used to track `Promise` -instances and asynchronous work scheduled by them. +instances and asynchronous work scheduled by them. The `Promise`s are only +tracked when `trackPromises` option is set to `true`. Users are able to define their own `type` when using the public embedder API. @@ -910,6 +913,38 @@ only on chained promises. That means promises not created by `then()`/`catch()` will not have the `before` and `after` callbacks fired on them. For more details see the details of the V8 [PromiseHooks][] API. +### Disabling promise execution tracking + +Tracking promise execution can cause a significant performance overhead. +To opt out of promise tracking, set `trackPromises` to `false`: + +```cjs +const { createHook } = require('node:async_hooks'); +const { writeSync } = require('node:fs'); +createHook({ + init(asyncId, type, triggerAsyncId, resource) { + // This init hook does not get called when trackPromises is set to false. + writeSync(1, `init hook triggered for ${type}\n`); + }, + trackPromises: false, // Do not track promises. +}).enable(); +Promise.resolve(1729); +``` + +```mjs +import { createHook } from 'node:async_hooks'; +import { writeSync } from 'node:fs'; + +createHook({ + init(asyncId, type, triggerAsyncId, resource) { + // This init hook does not get called when trackPromises is set to false. + writeSync(1, `init hook triggered for ${type}\n`); + }, + trackPromises: false, // Do not track promises. +}).enable(); +Promise.resolve(1729); +``` + ## JavaScript embedder API Library developers that handle their own asynchronous resources performing tasks @@ -934,7 +969,7 @@ The documentation for this class has moved [`AsyncLocalStorage`][]. [`Worker`]: worker_threads.md#class-worker [`after` callback]: #afterasyncid [`before` callback]: #beforeasyncid -[`createHook`]: #async_hookscreatehookcallbacks +[`createHook`]: #async_hookscreatehookoptions [`destroy` callback]: #destroyasyncid [`executionAsyncResource`]: #async_hooksexecutionasyncresource [`init` callback]: #initasyncid-type-triggerasyncid-resource diff --git a/lib/async_hooks.js b/lib/async_hooks.js index 3e3982a7ac61e5..5922971b9be68b 100644 --- a/lib/async_hooks.js +++ b/lib/async_hooks.js @@ -18,6 +18,8 @@ const { ERR_ASYNC_CALLBACK, ERR_ASYNC_TYPE, ERR_INVALID_ASYNC_ID, + ERR_INVALID_ARG_TYPE, + ERR_INVALID_ARG_VALUE, } = require('internal/errors').codes; const { kEmptyObject, @@ -71,7 +73,7 @@ const { // Listener API // class AsyncHook { - constructor({ init, before, after, destroy, promiseResolve }) { + constructor({ init, before, after, destroy, promiseResolve, trackPromises }) { if (init !== undefined && typeof init !== 'function') throw new ERR_ASYNC_CALLBACK('hook.init'); if (before !== undefined && typeof before !== 'function') @@ -82,13 +84,25 @@ class AsyncHook { throw new ERR_ASYNC_CALLBACK('hook.destroy'); if (promiseResolve !== undefined && typeof promiseResolve !== 'function') throw new ERR_ASYNC_CALLBACK('hook.promiseResolve'); + if (trackPromises !== undefined && typeof trackPromises !== 'boolean') { + throw new ERR_INVALID_ARG_TYPE('trackPromises', 'boolean', trackPromises); + } this[init_symbol] = init; this[before_symbol] = before; this[after_symbol] = after; this[destroy_symbol] = destroy; this[promise_resolve_symbol] = promiseResolve; - this[kNoPromiseHook] = false; + if (trackPromises === false) { + if (promiseResolve) { + throw new ERR_INVALID_ARG_VALUE('trackPromises', + trackPromises, 'must not be false when promiseResolve is enabled'); + } + this[kNoPromiseHook] = true; + } else { + // Default to tracking promises for now. + this[kNoPromiseHook] = false; + } } enable() { diff --git a/lib/internal/inspector_async_hook.js b/lib/internal/inspector_async_hook.js index db78e05f0a1da9..8ab833ea8ef1e9 100644 --- a/lib/internal/inspector_async_hook.js +++ b/lib/internal/inspector_async_hook.js @@ -7,7 +7,6 @@ function lazyHookCreation() { const inspector = internalBinding('inspector'); const { createHook } = require('async_hooks'); config = internalBinding('config'); - const { kNoPromiseHook } = require('internal/async_hooks'); hook = createHook({ init(asyncId, type, triggerAsyncId, resource) { @@ -30,8 +29,8 @@ function lazyHookCreation() { destroy(asyncId) { inspector.asyncTaskCanceled(asyncId); }, + trackPromises: false, }); - hook[kNoPromiseHook] = true; } function enable() { diff --git a/test/async-hooks/test-track-promises-default.js b/test/async-hooks/test-track-promises-default.js new file mode 100644 index 00000000000000..071f8faaecc712 --- /dev/null +++ b/test/async-hooks/test-track-promises-default.js @@ -0,0 +1,16 @@ +'use strict'; +// Test that trackPromises default to true. +const common = require('../common'); +const { createHook } = require('node:async_hooks'); +const assert = require('node:assert'); + +let res; +createHook({ + init: common.mustCall((asyncId, type, triggerAsyncId, resource) => { + assert.strictEqual(type, 'PROMISE'); + res = resource; + }), +}).enable(); + +const promise = Promise.resolve(1729); +assert.strictEqual(res, promise); diff --git a/test/async-hooks/test-track-promises-false-check.js b/test/async-hooks/test-track-promises-false-check.js new file mode 100644 index 00000000000000..7ff4142d0d1ff5 --- /dev/null +++ b/test/async-hooks/test-track-promises-false-check.js @@ -0,0 +1,19 @@ +// Flags: --expose-internals +'use strict'; +// Test that trackPromises: false prevents promise hooks from being installed. + +require('../common'); +const { internalBinding } = require('internal/test/binding'); +const { getPromiseHooks } = internalBinding('async_wrap'); +const { createHook } = require('node:async_hooks'); +const assert = require('node:assert'); + +createHook({ + init() { + // This can get called for writes to stdout due to the warning about internals. + }, + trackPromises: false, +}).enable(); + +Promise.resolve(1729); +assert.deepStrictEqual(getPromiseHooks(), [undefined, undefined, undefined, undefined]); diff --git a/test/async-hooks/test-track-promises-false.js b/test/async-hooks/test-track-promises-false.js new file mode 100644 index 00000000000000..1a3b5eda2e2f3c --- /dev/null +++ b/test/async-hooks/test-track-promises-false.js @@ -0,0 +1,11 @@ +'use strict'; +// Test that trackPromises: false works. +const common = require('../common'); +const { createHook } = require('node:async_hooks'); + +createHook({ + init: common.mustNotCall(), + trackPromises: false, +}).enable(); + +Promise.resolve(1729); diff --git a/test/async-hooks/test-track-promises-true.js b/test/async-hooks/test-track-promises-true.js new file mode 100644 index 00000000000000..cc53f3a9c926e8 --- /dev/null +++ b/test/async-hooks/test-track-promises-true.js @@ -0,0 +1,17 @@ +'use strict'; +// Test that trackPromises: true works. +const common = require('../common'); +const { createHook } = require('node:async_hooks'); +const assert = require('node:assert'); + +let res; +createHook({ + init: common.mustCall((asyncId, type, triggerAsyncId, resource) => { + assert.strictEqual(type, 'PROMISE'); + res = resource; + }), + trackPromises: true, +}).enable(); + +const promise = Promise.resolve(1729); +assert.strictEqual(res, promise); diff --git a/test/async-hooks/test-track-promises-validation.js b/test/async-hooks/test-track-promises-validation.js new file mode 100644 index 00000000000000..108f82ff2562ce --- /dev/null +++ b/test/async-hooks/test-track-promises-validation.js @@ -0,0 +1,25 @@ +'use strict'; +// Test validation of trackPromises option. + +require('../common'); +const { createHook } = require('node:async_hooks'); +const assert = require('node:assert'); +const { inspect } = require('util'); + +for (const invalid of [0, null, 1, NaN, Symbol(0), function() {}, 'test']) { + assert.throws( + () => createHook({ + init() {}, + trackPromises: invalid, + }), + { code: 'ERR_INVALID_ARG_TYPE' }, + `trackPromises: ${inspect(invalid)} should throw`); +} + +assert.throws( + () => createHook({ + trackPromises: false, + promiseResolve() {}, + }), + { code: 'ERR_INVALID_ARG_VALUE' }, + `trackPromises: false and promiseResolve() are incompatible`); From 3759c9e3b39cd36cad07b080cfe55ad4d2e2ee77 Mon Sep 17 00:00:00 2001 From: Joyee Cheung Date: Mon, 19 Jan 2026 16:49:37 +0100 Subject: [PATCH 2/2] fixup! async_hooks: add trackPromises option to createHook() --- tools/doc/type-parser.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/doc/type-parser.mjs b/tools/doc/type-parser.mjs index 5af6e86651af28..db85a64e33a903 100644 --- a/tools/doc/type-parser.mjs +++ b/tools/doc/type-parser.mjs @@ -66,7 +66,7 @@ const customTypesMap = { 'AsyncLocalStorage': 'async_context.html#class-asynclocalstorage', - 'AsyncHook': 'async_hooks.html#async_hookscreatehookcallbacks', + 'AsyncHook': 'async_hooks.html#async_hookscreatehookoptions', 'AsyncResource': 'async_hooks.html#class-asyncresource', 'brotli options': 'zlib.html#class-brotlioptions',