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.
Reproduction (isolated, no commander involved)
const config: any = {
writeErr: (str: string) => process.stderr.write(str),
outputError: (str: string, write: (s: string) => void) => write(str),
};
function fireError(cfg: any, message: string) {
cfg.outputError(message, cfg.writeErr);
}
fireError(config, "error: something went wrong\n");
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.
Found while validating the removal of the native
commanderbinding (#10686) — with the bindinggone,
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, thecommander.missingArgument/commander.unknownOptionerrorcodes thrown) — except the error-message text commander writes to stderr before throwing, which
Perry silently drops.
Reproduction (isolated, no commander involved)
This is exactly the shape
commander's own_displayErroruses(
node_modules/commander/lib/command.js): a defaultoutputConfigurationobject holds two functionproperties,
writeErr: (str) => process.stderr.write(str)andoutputError: (str, write) => write(str), and error-reporting callsthis._outputConfiguration.outputError(message, this._outputConfiguration.writeErr)— anobject-property function invoking a SECOND object-property function passed to it as a parameter.
Isolating further
process.stderr.writealone works fine (confirmed separately).cfg.writeErr(message)), works fine.outputError(itself read off anobject) invoking its own parameter
write(itself a closure read off the same object and passedthrough one level of plain-function call). The inner
write(str)call insideoutputError's bodynever 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 printedtext — though the actual thrown
CommanderError(name,code,exitCode) is unaffected, sincethat'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'sacceptance test.