Skip to content

Errors and Exit Codes ​

A command can fail in a way that it expects, such as when the user asks for a project that does not exist, or in a way that it does not, such as a bug.

This guide shows how Gunshi reports each of them, and which exit code the process ends with.

Argument Errors ​

When the arguments do not match the command, such as a missing required argument or an option value of the wrong type, Gunshi does not run the command.

It prints the errors to stderr, without a stack trace, and sets the exit code to 1:

sh
$ node cli.js
Positional argument 'project' is required

The cli() promise resolves. To report the argument errors in your own way, see Reporting Errors Yourself.

Expected Failures ​

When a command fails in a way that it expects, throw a CommandError with a message for the user:

cli.js
js
import { cli, CommandError, define } from 'gunshi'

const projects = {
  acme: 'The website of Acme'
}

const command = define({
  name: 'show',
  args: {
    project: {
      type: 'positional',
      description: 'Name of the project'
    }
  },
  run: ctx => {
    const { project } = ctx.values

    if (!Object.hasOwn(projects, project)) {
      throw new CommandError(`Project ${project} not found`, { code: 'not_found', exitCode: 2 })
    }

    console.log(projects[project])
  }
})

await cli(process.argv.slice(2), command)

Gunshi prints the message to stderr, without a stack trace, and sets the exit code to the exitCode of the error:

sh
$ node cli.js nope
Project nope not found
$ echo $?
2

TIP

The example fully code is here.

The message may have more than one line, such as a line that tells the user what to do next, and Gunshi prints all of them.

CommandError takes these options, and all of them are optional:

OptionDescription
codeThe name of the failure, for programs to read, such as not_found or network_error. Write it in snake_case. Gunshi does not print it to users: with --json, it is the reason of the JSON error, as it is.
exitCodeThe exit code of the process, an integer from 1 to 255. Defaults to 1, and any other value is replaced with 1.
causeThe error that caused the failure. Gunshi prints its message after the message of the CommandError, and in data.causes with --json.

Causes ​

When a failure comes from another error, pass that error as the cause.

Gunshi prints its message on a Caused by: line, and does the same for the cause of the cause:

js
async function readProjects(file) {
  try {
    return JSON.parse(await readFile(file, 'utf8'))
  } catch (error) {
    throw new CommandError(`Could not read the projects from ${file}`, { cause: error })
  }
}
sh
$ node cli.js acme --file missing.json
Could not read the projects from missing.json
Caused by: ENOENT: no such file or directory, open 'missing.json'

The i18n plugin translates the Caused by heading, with the CAUSEDBY key of the built-in resources.

WARNING

Gunshi prints the message of the cause as it is, for people and in the JSON error of --json. Do not pass an error whose message may contain a secret, such as a URL with a token, as the cause. Write a message that is safe to print in the CommandError instead.

Where to Throw It ​

Gunshi prints a CommandError that is thrown from these places:

  • run, and the functions that run calls, including the functions of a plugin extension
  • a command decorator of a plugin (see Plugin Decorators)

It does not print a CommandError thrown from a hook, while a plugin extends the command context, or while a lazy command loads.

These run outside the command, and the cli() promise rejects with the error, as it does with an unexpected error. Throw the failures that a command expects from run.

With --json ​

A command that spreads jsonArgs prints its errors as one line of JSON to stderr with --json, and stdout is empty. The code of a CommandError is the reason of the JSON error, and its exit code is kept:

sh
$ node cli.js show nope --json
{"status":"error","reason":"not_found","message":"Project nope not found"}

Any other error that the command throws is printed as JSON too, as command_failed with exit code 1, and the cli() promise resolves. For the shape of the JSON errors, see JSON Output.

Unexpected Errors ​

Without --json, Gunshi does not print an error other than CommandError.

The cli() promise rejects with the error, and unless the app catches it, the runtime prints the stack trace and ends with exit code 1.

A bug stays visible, with the stack trace to find it.

Exit Codes ​

What happensExit codeThe cli() promise
The command succeeds0, unless the command sets process.exitCodeResolves
The arguments are wrong1Resolves
The command throws a CommandErrorThe exitCode of the error, 1 by defaultResolves
The command throws any other error1, when the app does not catch the errorRejects

Gunshi sets the exit code with process.exitCode, and does not call process.exit().

The process ends when nothing is left running, so close the servers, the timers, and anything else that keeps it alive before you throw a CommandError.

After Gunshi Reports an Error ​

When Gunshi has reported an error, the cli() promise resolves, so a catch around cli() does not run for it.

Clean up or record the failure in the onErrorCommand hook instead. Gunshi calls the hook after it prints the error and sets the exit code, so the hook can also change the exit code:

js
import { cli, isCommandError } from 'gunshi'

await cli(process.argv.slice(2), command, {
  onErrorCommand: (ctx, error) => {
    // print the stack trace of an expected failure too, while debugging
    if (process.env.DEBUG && isCommandError(error)) {
      console.error(error.stack)
    }
  }
})

Tell a CommandError with isCommandError(), rather than with instanceof. The packages of Gunshi, such as gunshi, @gunshi/plugin, and @gunshi/definition, bundle their own copies of the class, and instanceof does not match an error created by another copy.

For more about the hook, see Command Hooks.

Reporting Errors Yourself ​

With usageSilent: true, Gunshi prints nothing and leaves the exit code alone.

The cli() promise rejects with the CommandError, or with the AggregateError that holds the argument errors, for you to report them. This suits tests, and programs that call cli().

To report only the argument errors yourself, set renderValidationErrors to null (see Handling Argument Validation Errors). It does not change how Gunshi reports a CommandError.

Released under the MIT License.