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.

PR-URL: https://github.com/nodejs/node/pull/61415
Refs: https://github.com/nodejs/node/pull/57148
Reviewed-By: Gerhard Stöbich <deb2001-github@yahoo.de>
This commit is contained in:
Joyee Cheung authored and GitHub committed 2026-01-28 11:25:24 +00:00
1 parent 37e40040a7
commit 8c380ded46
9 files changed
+145 -9

No files matched your search

+39 -4
View File
@@ -144,18 +144,20 @@ function destroy(asyncId) { }
function promiseResolve(asyncId) { }
```
## `async_hooks.createHook(callbacks)`
## `async_hooks.createHook(options)`
<!-- YAML
added: v8.1.0
-->
* `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
+16 -2
View File
@@ -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() {
+1 -2
View File
@@ -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() {
@@ -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);
@@ -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]);
@@ -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);
@@ -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);
@@ -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`);
+1 -1
View File
@@ -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',