Skip to content

fix(cli): an error thrown by an async command prints a Node stack trace #1270

Closed

Description

@SaulMoro

What happens

When an async command action throws, the CLI prints a raw Node stack trace instead of one error line. src/index.ts ends with program.parse(). Commander does not await async actions under parse(), so a rejected action becomes an unhandled rejection. Node then prints the error with its stack and exits 1.

Before a fix in the #1084 work, every teamai session command on a user-scope install showed this. They threw No teamai project config found… with a stack trace. That case is fixed, but any other error an async action throws still reaches users the same way.

Expected

An error from any command prints one actionable line on stderr, and the command exits 1. A stack trace appears only with a debug flag or environment variable.

Suggested fix

Replace program.parse() with await program.parseAsync() inside one catch that prints error.message through the logger and sets process.exitCode = 1. Keep the stack behind the existing debug switch.

Commands that already call process.exit keep working. A test that spies on process.exit for a specific command (for example the session pull failure test in session-cmd.test.ts) is unaffected.

Activity

  1. Smilewithoutfalling commented on Oct 22, 2026

    @Smilewithoutfalling
    Contributor

    Thanks — picked this up in #1271.

    One correction to the repro path in the description: the boundary on main is already await program.parseAsync(), not a bare parse(), so the sync-parse framing no longer applies. The leak is on the async side. The boundary only special-cases NotInitializedError and rethrows everything else, so any command action that throws a plain Error escapes as an unhandled rejection and Node prints it under a cli.js:NNNN header followed by the whole stack. Current repro on main is teamai session list in a directory that is not initialised.

    The fix moves that whole job to the boundary. reportCommandError logs the message as one line through log.error, sets process.exitCode = 1, and sends the stack to log.debug, which always appends to ~/.teamai/debug.log and reaches the console only under --verbose. reportNotInitialized is unchanged; the boundary is what now covers the commands that never wrapped themselves.

    Verified with unit tests for each branch (message plus exit code, stack routed to debug, empty-message fallback, non-Error values) and a real-CLI e2e that spawns the built binary: one line on stderr, no cli.js: prefix, no at frames, empty stdout, and under -v the stack on stdout. Reverting only the src/index.ts change makes the new e2e fail 2/2, so the test is pinned to the fix rather than to the local environment.

  2. added a commit that references this issue on Oct 22, 2026
    6bcdaf3
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions