CLI Development
CLI Error Handling
The CLI shows expected errors as clean messages without a stack trace, and shows the full traceback for unexpected bugs.
Error Classes (src/control_tower/utils/errors.py)
ExpectedError- Base class for all user-facing errors. These are caught at the CLI layer and displayed cleanly.ValidationError- User provided conflicting options or invalid input.StateError- Required precondition not met (missing resource, config, or setup).OperationError- An external operation (subprocess, container command) failed.
Using Errors in CLI Commands
Apply the @handle_user_errors decorator immediately after @click.command() (or @group.command()). Decorators execute top-to-bottom during invocation, so placed lower in the stack it won't catch errors from the decorators above it:
from control_tower.cli.base import handle_user_errors from control_tower.utils.errors import StateError, ValidationError @click.command() @handle_user_errors # Must be right after @click.command() @click.option("--some-option", help="...") @other_decorators() def my_command(option_a: bool, option_b: bool): if option_a and option_b: raise ValidationError("`option_a` and `option_b` cannot both be set") if not some_precondition(): raise StateError("Context not configured. Run 'ct live up' first")
The decorator:
- Converts
ExpectedErrorexceptions toclick.ClickExceptionfor clean display - Transforms parameter names in messages to CLI equivalents:
- Backtick-wrapped names:
`option_a`→--option-a - Underscore names:
option_a→--option-a
- Backtick-wrapped names:
- Adds a hidden
--tracebackflag that shows the full stack trace for any error
Guidelines
- Use
ExpectedErrorsubclasses for errors the user can understand and potentially fix - Use standard exceptions (
RuntimeError,ValueError, etc.) for unexpected cases that need stack traces - Wrap parameter names in backticks in error messages so they are converted to CLI names; names without an underscore are only converted when backticked
- Keep error messages concise and actionable