Skip to content

All CLI errors print a raw Node stack trace — bin/run passes the @oclif/core/handle module object (not .handle) to .catch() #829

Description

@pru55e11

Summary

Every CLI error in aio is printed as a raw Node.js uncaught-exception dump (source frame, ^ caret, full stack, Node.js vXX footer) instead of oclif's clean › Error: <message> formatting. This affects all error paths — arg/flag parse errors, this.error() validation, and command runtime errors alike.

Root cause: bin/run passes the @oclif/core/handle module object to Promise.prototype.catch() instead of its handle function. Since @oclif/core v4 exports handle as a named export ({ handle, Exit }), .catch(<object>) is a no-op per the Promise spec, the rejection goes unhandled, and Node prints the raw stack and exits.

Steps to reproduce

$ aio runtime ip-list get --region MARS
/Users/.../@oclif/core/lib/errors/error.js:44
        err = new cli_1.CLIError(input, options);
              ^
CLIError: invalid region "MARS". Expected one of: amer, emea, apac, aus
    at Object.error (.../@oclif/core/lib/errors/error.js:44:15)
    ...
    at async AIOCommand.run (.../@adobe/aio-cli/src/index.js:38:12)

Node.js v24.14.1

Reproduces on any command that errors, e.g.:

  • aio runtime:action:invokeRequiredArgsError: Missing 1 required arg (raw stack)
  • aio runtime property get --bogusflagNonExistentFlagsError: Nonexistent flag (raw stack)

Expected: a clean, single-line message, e.g. › Error: invalid region "MARS". Expected one of: amer, emea, apac, aus.

Root cause

bin/run:

require('../src/').run()
  .then(() => require('@oclif/core/flush'))
  .catch(require('@oclif/core/handle'))

In @oclif/core v4, the ./handle and ./flush subpath exports are modules with named exports, not callable functions:

> require('@oclif/core/handle')
{ Exit: { exit: [Function] }, handle: [Function: handle] }   // typeof === 'object', not callable
> require('@oclif/core/flush')
{ flush: [Function: flush] }                                  // typeof === 'object', not callable

So:

  1. .catch(require('@oclif/core/handle')) hands .catch() a non-function. Per the Promise spec a non-function onRejected is ignored, the rejection propagates as an unhandled promise rejection, and Node (which treats unhandled rejections as fatal since v15, default-throw hardened in later releases) prints the raw stack and exits. oclif's pretty-printer (pretty-print › Error:) is never reached.
  2. .then(() => require('@oclif/core/flush')) only requires the flush module and returns it — flush() is never called, so buffered stdout may not be flushed before exit.

This regressed when @adobe/aio-cli moved to @oclif/core v4 (current dep: @oclif/core: ^4.9.0). In @oclif/errors / older @oclif/core, handle/flush were exported as callable defaults, so the old bin/run worked. With v4's named exports the wiring silently breaks.

Proof

// reproduces the raw stack — object passed to .catch() is ignored, rejection is unhandled
Promise.reject(new Error('boom')).catch(require('@oclif/core/handle'))

// clean — the actual function handles it (prints "  Error: boom" and exits)
Promise.reject(Object.assign(new Error('boom'), { oclif: { exit: 1 } }))
  .catch(require('@oclif/core/handle').handle)

Suggested fix

Pass the functions, not the module objects:

require('../src/').run()
  .then(() => require('@oclif/core/flush').flush())
  .catch((error) => require('@oclif/core/handle').handle(error))

(or destructure: const { handle } = require('@oclif/core/handle') / const { flush } = require('@oclif/core/flush').)

Impact

Environment

  • @adobe/aio-cli 11.1.0 (also present on master)
  • @oclif/core 4.11.4 (dep ^4.9.0)
  • Node.js v24.14.1 (mechanism applies to Node ≥ 15; consistently fatal on Node 22+)
  • macOS (Darwin arm64)

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions