Errors
Every CLI will eventually fail. The difference between a good CLI and a frustrating one is what happens when it does.
What Makes a Good Error?
Compare these:
Bad:
Error: ENOENTBetter:
Error: File not found: config.jsonBest:
Error: Config file not found: ./config.json
Looked in:
./config.json
~/.config/mycli/config.json
Try: mycli init --config to create oneA good error tells you:
- What went wrong
- Where it went wrong (which file, which flag, which step)
- What to do about it
That third part — the suggestion — is what separates tools people love from tools people dread.
Categories of Errors
User Errors
The user typed something wrong:
Error: Unknown flag '--region'. Did you mean '--remote'?
Error: Missing required argument: <filename>
Error: Invalid value for --port: "abc" is not a numberThese should:
- Reference the exact flag/argument that's wrong
- Suggest corrections ("did you mean?")
- Exit with code 2 (misuse)
- Never print stack traces
Runtime Errors
Something failed during execution:
Error: Connection refused: https://api.example.com
Error: Permission denied: /etc/secretsThese should:
- Describe what the program was trying to do
- Include relevant context (URL, file path, etc.)
- Suggest a fix when possible ("check your network", "run with sudo")
- Exit with code 1
Internal Errors
Bugs in the program itself:
Internal error: unexpected null in parseConfig
Please report this at: https://github.com/...These are rare in production but should:
- Clearly say it's a bug, not the user's fault
- Include enough info to file a bug report
- Not swallow the actual error
"Did You Mean?"
One of the highest-value features in a CLI: fuzzy matching for typos.
$ mycli delpoy
Error: Unknown command 'delpoy'. Did you mean 'deploy'?This catches the most common user error — typos — and gives an instant fix. Most users will retype the command correctly without even reading the rest of the error message.
This works for:
- Command names
- Flag names
- Enum values
Errors in JSON Mode
When a CLI has --json mode, errors should also be structured:
$ mycli check --json{
"error": {
"message": "Config file not found",
"code": "CONFIG_NOT_FOUND",
"suggest": "Run `mycli init` to create a config file"
}
}Scripts parsing JSON output can check for the error field instead of parsing human-readable text.
stderr, Not stdout
Errors go to stderr, not stdout. This seems minor but matters when piping: :::
mycli list | jq '.[]'If the error went to stdout, jq would try to parse Error: something as JSON and fail with a confusing error of its own. With errors on stderr, they show up in the terminal while stdout stays clean.
Error Codes
A string error code (CONFIG_NOT_FOUND, AUTH_REQUIRED) is more useful than a number for programmatic handling:
// Scripts can match on the code
if (result.error.code === 'AUTH_REQUIRED') {
// refresh token and retry
}Human messages change between versions. Error codes are stable identifiers that scripts can rely on.
What's Next?
- Testing CLIs — how to test all these error paths
- Error Handling guide — implementing error handling in dreamcli