Repository navigation
Calling undefined as a function silently no-ops instead of throwing TypeError #648
Description
Repro
const obj: any = {};
try {
obj.missingFn(1, 2);
console.log("did NOT throw");
} catch (e) {
console.log("threw:", (e as Error).message);
}Actual (Perry)
did NOT throw
Expected (Node)
threw: obj.missingFn is not a function
Impact
This is the single biggest leverage fix for the parity test suite. Calling a non-existent property as a function should throw TypeError: <expr> is not a function. Perry silently no-ops, which causes a cascading set of failures across the runtime parity tests:
-
test_parity_timersHANGS forever — the test doesawait new Promise(r => timers.setTimeout(cb, 0, 2, 3)). Since Namespace import for unrecognized node:* sub-paths resolves totrue(TAG_TRUE) #629's partial fix,timersis an empty object instead of a boolean, sotimers.setTimeoutisundefined. The executor callsundefined(cb, 0, 2, 3)which Perry no-ops;resolvenever fires; the await pends forever. Yesterday's binary threwTypeError: (boolean).setTimeout is not a functionand the surrounding try/catch handled it cleanly. Today the test must be killed by the runner timeout. -
Silent mid-test termination in many parity modules —
test_parity_osfinishes after 15 of 60 expected lines;test_parity_tlsafter 36 of 76;test_parity_perf_hooksafter 43 of 84;test_parity_http2after 62 of 137. In each case Perry runs into an undefined call mid-script and silently exits the surrounding async path without surfacing the error to the diff. -
Confusing parity output — many user-visible bugs end up looking like "Perry produces fewer lines than Node" rather than the more honest "Perry threw at line X of test Y". A correct TypeError would be caught by per-section try/catch in the parity tests and surface as a one-line
ERROR:row, dramatically improving the gap report quality.
Why this changed
Yesterday Perry NaN-boxed unrecognized node:* namespaces as TAG_TRUE (#629). Calling (true).foo() is a property access on a primitive boolean, which Node correctly throws on — and Perry's existing primitive-method-dispatch path threw too. After #629's partial fix flipped the namespace to an empty object, the path now goes through Perry's object-property lookup, which returns undefined. Calling undefined() is a separate code path that silently no-ops.
So the regression isn't in #629's change itself — it's that #629's fix exposed an existing latent bug in Perry's "call undefined as function" handling.
Acceptance
The 6-line repro above prints threw: obj.missingFn is not a function matching Node, AND test_parity_timers runs to completion without timing out (regardless of whether timers.setTimeout is actually implemented).
Related
- Indirectly enabled by Namespace import for unrecognized node:* sub-paths resolves to
true(TAG_TRUE) #629's partial fix (namespace switched from TAG_TRUE → empty object). - Cascades into ~10 of 38 parity tests that silently truncate mid-script.
- Same family as typeof process returns "number" instead of "object" (default-import + global) #623 (typeof returns wrong value for unrecognized binding) but on the call path, not the typeof path.
Activity
fix: closes #645 #648 #649 + partial #650 — refs #647 (v0.5.770)
proggeramlug commentedon May 10, 2026
Fixed in 3ef40d3 (v0.5.770).
js_closure_callN(and_array/_apply_with_spread) andjs_native_call_method's catch-all now throw<value>/<method> is not a functionviajs_throw_type_error_not_a_function, routed through Perry's exception system sotry/catchcatches it. The 6-line repro now printsthrew: missingFn is not a function. Fallout: added explicitfs.rmSyncHIR lowering since the catch-all previously masked it as a silent no-op. Sanity: 27/28 gap tests byte-identical to Node.fix: refs #645 deeper followup — three coordinated bugs blocking driz…