CLI Development

CLI Error Handling

The CLI uses a structured error system to provide clean user-facing error messages without stack traces for expected errors, while still showing full tracebacks for unexpected bugs.

Debugging Errors

Commands decorated with @handle_user_errors support a hidden --traceback flag that shows the full stack trace for any error.

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 to Click commands immediately after @click.command():

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 @handle_user_errors decorator must be placed immediately after @click.command() (or @group.command()) because decorators execute top-to-bottom during invocation, so placing it lower in the stack means it won't catch errors from decorators above it.

The decorator:

  1. Converts ExpectedError exceptions to click.ClickException for clean display
  2. Transforms parameter names in messages to CLI equivalents:
    • Backtick-wrapped names: `option_a`--option-a
    • Underscore names: option_a--option-a
  3. Adds the --traceback flag to the command

Guidelines

  • Use ExpectedError subclasses for errors the user can understand and potentially fix
  • Use standard exceptions (RuntimeError, ValueError, etc.) for unexpected cases that need stack traces
  • Reference parameters with backticks (at least if it doesn't have an underscore) in error messages for automatic CLI name transformation
  • Keep error messages concise and actionable