You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
A function read from an object property silently drops its own call to a second function passed to it as a parameter (also read from an object property) — commander error-message output is lost #10711
Found while validating the removal of the native commander binding (#10686) — with the binding
gone, import { Command } from "commander" now compiles the real npm package from source (via #10699). Every value checked matches Node 26.5.1 exactly (program.args, boolean option defaults,
subcommand .action() callbacks, the commander.missingArgument/commander.unknownOption error
codes thrown) — except the error-message text commander writes to stderr before throwing, which
Perry silently drops.
node main.ts # prints "error: something went wrong"
perry compile main.ts -o out && ./out # prints nothing
This is exactly the shape commander's own _displayError uses
(node_modules/commander/lib/command.js): a default outputConfiguration object holds two function
properties, writeErr: (str) => process.stderr.write(str) and outputError: (str, write) => write(str), and error-reporting calls this._outputConfiguration.outputError(message, this._outputConfiguration.writeErr) — an
object-property function invoking a SECOND object-property function passed to it as a parameter.
Isolating further
process.stderr.write alone works fine (confirmed separately).
A function stored as an object property, called directly (cfg.writeErr(message)), works fine.
The failure is specific to the two-level indirection: outputError (itself read off an
object) invoking its own parameter write (itself a closure read off the same object and passed
through one level of plain-function call). The inner write(str) call inside outputError's body
never executes — no output, no crash, no error.
Impact
Any code that stores a callback on an object and calls it indirectly through a second function
(the "strategy"/"configurable output" pattern commander, and likely other npm packages, use) will
silently drop the inner call. For commander specifically: program.error(...), exitOverride-driven validation failures (missing required argument, unknown option), and --help/version output that routes through the same configuration object all lose their printed
text — though the actual thrown CommanderError (name, code, exitCode) is unaffected, since
that's a separate code path.
Not fixed by, and not caused by, #10686's binding removal — commander's own error codes, program.args, option defaults, and .action() dispatch all match Node exactly in that PR's
acceptance test.
I tried to reproduce this and could not, on any tree — including your own branch. Details
below, because the way it fails to reproduce points fairly directly at a build problem
rather than a compiler one.
main @ 8df83f8c — the commit your branch forks from
error: something went wrong
PR #10712 head 463c4fa5 (over #10699) — your actual tree
error: something went wrong
Each row is a separate full cargo build --release -p perry -p perry-runtime-static -p perry-stdlib-static with PERRY_RUNTIME_DIR pinned to the matching release/, so no
arm could have linked a stale archive. Byte-identical to Node in every case, and the text
really is on stderr (2>/dev/null swallows it, 2>&1 >/dev/null keeps it).
Real commander 14.0.3 compiled from source via perry.compilePackages also matches Node
byte for byte across the whole surface you list — --help, --version, missing required
argument, unknown option, unknown command and program.error() — under both the default
output configuration and a configureOutput() override, on main and on your tree alike.
I confirmed the real JS class was in use, not a shim (_outputConfiguration a live object, outputError/writeErr functions, constructor.name === "Command"). 32 further spellings
of the same indirection agree with Node too: method shorthand, class field, Object.assign
override, spread, nested receiver, cross-object writer, computed keys, array-element
receiver, getter-provided writer, Object.create chain, Object.freeze, destructuring,
three-level chaining, two plain-function hops, async caller, nested closure, loop, and the
shape inside a CommonJS module. A missing writer throws TypeError rather than being
dropped.
What does lose commander's output
The perry-ext-commander native shim — the path you get when commander is not in perry.compilePackages:
note: serving `commander` from the bundled native binding `perry-ext-commander`
(a partial drop-in), ignoring your installed `node_modules/commander`.
Against that shim the same program prints nothing at all — no error text, and no CommanderError either, so the catch never runs:
--help and --version are empty and program.args is undefined.
Why I think this is your build
"Codes right, text missing" is a blend of the two paths, and neither produces it on its
own. Compiled from source, both are right. Through the shim, both are wrong. A build
where part of the program reaches the shim and part reaches the real source produces
exactly what you saw.
That is what a stale libperry_stdlib.a looks like on a branch that removes perry-ext-commander and edits crates/perry-stdlib/Cargo.toml. perry-runtime and perry-stdlib are crate-type = ["rlib"], so cargo build -p perry-runtime -p perry-stdlib
does not emit libperry_{runtime,stdlib}.a — those come from the -static wrapper
crates. If they weren't rebuilt, perry compile links archives that still contain the old
commander binding while your compiler no longer agrees about who owns it.
Worth checking before anything else:
cargo build --release -p perry -p perry-runtime-static -p perry-stdlib-static # ONE invocation
ls -la <target>/release/libperry_runtime.a <target>/release/libperry_stdlib.a # mtime must have moved
Keep the package set identical across builds — dropping -p perry changes cargo feature
unification. Then confirm the note above is absent from the compile output.
Leaving this open — it's your call once you've checked the build. If it still reproduces
with freshly built archives, the exact perry compile invocation and the compiler/runtime
commit stamps would be the useful next thing.
I've put a regression lock on the shape in #10728 regardless. It passes on unfixed main,
so it does not discriminate — it's there because the _displayError indirection had no
coverage and #10689 (an inherited property read folding to undefined on a scalar-replaced
object) landed one commit before this was filed, is the same family, and was silent in the
same way.
Fixed by b2dce52c36 (changelog.d/10728-property-fn-param-callback-lock.md), on main as of v0.5.1618.
I verified with this issue's own reproducer, because I asserted the opposite in merge train 233's notes — at that time it genuinely was not fixed, and the fix landed afterwards:
node: error: something went wrong
perry: error: something went wrong
This also removes the caveat attached to the commander binding removal, which shipped noting that four stderr lines were lost to this defect.
Activity
proggeramlug commentedon Sep 19, 2026
I tried to reproduce this and could not, on any tree — including your own branch. Details
below, because the way it fails to reproduce points fairly directly at a build problem
rather than a compiler one.
Your isolated repro, run verbatim
node --experimental-strip-types)error: something went wrongmain@4715bc2f(v0.5.1598, train 220)error: something went wrongmain@8df83f8c— the commit your branch forks fromerror: something went wrong463c4fa5(over #10699) — your actual treeerror: something went wrongEach row is a separate full
cargo build --release -p perry -p perry-runtime-static -p perry-stdlib-staticwithPERRY_RUNTIME_DIRpinned to the matchingrelease/, so noarm could have linked a stale archive. Byte-identical to Node in every case, and the text
really is on stderr (
2>/dev/nullswallows it,2>&1 >/dev/nullkeeps it).Real commander 14.0.3 compiled from source via
perry.compilePackagesalso matches Nodebyte for byte across the whole surface you list —
--help,--version, missing requiredargument, unknown option, unknown command and
program.error()— under both the defaultoutput configuration and a
configureOutput()override, onmainand on your tree alike.I confirmed the real JS class was in use, not a shim (
_outputConfigurationa live object,outputError/writeErrfunctions,constructor.name === "Command"). 32 further spellingsof the same indirection agree with Node too: method shorthand, class field,
Object.assignoverride, spread, nested receiver, cross-object writer, computed keys, array-element
receiver, getter-provided writer,
Object.createchain,Object.freeze, destructuring,three-level chaining, two plain-function hops, async caller, nested closure, loop, and the
shape inside a CommonJS module. A missing writer throws
TypeErrorrather than beingdropped.
What does lose commander's output
The
perry-ext-commandernative shim — the path you get when commander is not inperry.compilePackages:Against that shim the same program prints nothing at all — no error text, and no
CommanderErroreither, so thecatchnever runs:--helpand--versionare empty andprogram.argsisundefined.Why I think this is your build
"Codes right, text missing" is a blend of the two paths, and neither produces it on its
own. Compiled from source, both are right. Through the shim, both are wrong. A build
where part of the program reaches the shim and part reaches the real source produces
exactly what you saw.
That is what a stale
libperry_stdlib.alooks like on a branch that removesperry-ext-commanderand editscrates/perry-stdlib/Cargo.toml.perry-runtimeandperry-stdlibarecrate-type = ["rlib"], socargo build -p perry-runtime -p perry-stdlibdoes not emit
libperry_{runtime,stdlib}.a— those come from the-staticwrappercrates. If they weren't rebuilt,
perry compilelinks archives that still contain the oldcommander binding while your compiler no longer agrees about who owns it.
Worth checking before anything else:
Keep the package set identical across builds — dropping
-p perrychanges cargo featureunification. Then confirm the note above is absent from the compile output.
Leaving this open — it's your call once you've checked the build. If it still reproduces
with freshly built archives, the exact
perry compileinvocation and the compiler/runtimecommit stamps would be the useful next thing.
I've put a regression lock on the shape in #10728 regardless. It passes on unfixed
main,so it does not discriminate — it's there because the
_displayErrorindirection had nocoverage and #10689 (an inherited property read folding to
undefinedon a scalar-replacedobject) landed one commit before this was filed, is the same family, and was silent in the
same way.
test(gap): lock commander's outputError/writeErr indirection (#10711)
proggeramlug commentedon Sep 20, 2026
Fixed by
b2dce52c36(changelog.d/10728-property-fn-param-callback-lock.md), onmainas of v0.5.1618.I verified with this issue's own reproducer, because I asserted the opposite in merge train 233's notes — at that time it genuinely was not fixed, and the fix landed afterwards:
This also removes the caveat attached to the
commanderbinding removal, which shipped noting that four stderr lines were lost to this defect.