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:
$ node cli.js
Positional argument 'project' is requiredThe 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:
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:
$ node cli.js nope
Project nope not found
$ echo $?
2TIP
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:
| Option | Description |
|---|---|
code | The 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. |
exitCode | The exit code of the process, an integer from 1 to 255. Defaults to 1, and any other value is replaced with 1. |
cause | The 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:
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 })
}
}$ 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 thatruncalls, 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:
$ 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 happens | Exit code | The cli() promise |
|---|---|---|
| The command succeeds | 0, unless the command sets process.exitCode | Resolves |
| The arguments are wrong | 1 | Resolves |
The command throws a CommandError | The exitCode of the error, 1 by default | Resolves |
| The command throws any other error | 1, when the app does not catch the error | Rejects |
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:
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.
