Summary
async_hooks.createHook(options) should match Node's validation for the top-level options value and hook callback members. Perry currently reads callback fields permissively and creates a hook even when options is undefined/null or when init, before, after, destroy, or promiseResolve are present but non-callable.
Node behavior
Probe run with npx --yes node@25.9.0:
const async_hooks = require("node:async_hooks");
for (const value of [undefined, null]) {
try { async_hooks.createHook(value); }
catch (err) { console.log("options", String(value), err.name, err.code || "no-code", err.message); }
}
for (const value of [null, 0, true, "x", {}, [], Symbol("s")]) {
try { async_hooks.createHook({ init: value }); }
catch (err) { console.log("init", String(value), err.name, err.code || "no-code", err.message); }
}
console.log(typeof async_hooks.createHook({ init() {} }).enable);
Observed behavior:
createHook(undefined) and createHook(null) throw TypeError before creating a hook.
- Present non-callable hook members throw
TypeError [ERR_ASYNC_CALLBACK] with messages like hook.init must be a function.
- A missing/
undefined member is allowed, and primitive top-level objects like 0 are accepted because destructuring simply produces no callback fields.
Perry implementation evidence
crates/perry-codegen/src/lower_call/native_table/async_decimal.rs routes async_hooks.createHook to js_async_hooks_create_hook with a raw NA_F64 options value.
crates/perry-runtime/src/async_hooks.rs::object_field() returns undefined when the options value is not an object pointer.
callbacks_from_options() reads init, before, after, destroy, and promiseResolve, then assigns each with closure_from_value(...) without checking that a present value is callable.
js_async_hooks_create_hook() always pushes a HookRecord with those callbacks and returns an AsyncHookHandle.
Expected fix
- Preserve Node's allowed cases: missing/
undefined individual callback fields and non-nullish primitive top-level options should not register callbacks.
- Reject nullish top-level options like Node does.
- Reject present non-callable callback fields with
TypeError [ERR_ASYNC_CALLBACK] naming the invalid hook member.
- Add parity coverage for nullish options and invalid
init/before/after/destroy/promiseResolve members.
Summary
async_hooks.createHook(options)should match Node's validation for the top-level options value and hook callback members. Perry currently reads callback fields permissively and creates a hook even whenoptionsisundefined/nullor wheninit,before,after,destroy, orpromiseResolveare present but non-callable.Node behavior
Probe run with
npx --yes node@25.9.0:Observed behavior:
createHook(undefined)andcreateHook(null)throwTypeErrorbefore creating a hook.TypeError [ERR_ASYNC_CALLBACK]with messages likehook.init must be a function.undefinedmember is allowed, and primitive top-level objects like0are accepted because destructuring simply produces no callback fields.Perry implementation evidence
crates/perry-codegen/src/lower_call/native_table/async_decimal.rsroutesasync_hooks.createHooktojs_async_hooks_create_hookwith a rawNA_F64options value.crates/perry-runtime/src/async_hooks.rs::object_field()returnsundefinedwhen the options value is not an object pointer.callbacks_from_options()readsinit,before,after,destroy, andpromiseResolve, then assigns each withclosure_from_value(...)without checking that a present value is callable.js_async_hooks_create_hook()always pushes aHookRecordwith those callbacks and returns anAsyncHookHandle.Expected fix
undefinedindividual callback fields and non-nullish primitive top-level options should not register callbacks.TypeError [ERR_ASYNC_CALLBACK]naming the invalid hook member.init/before/after/destroy/promiseResolvemembers.