error-messages
$
npx mdskill add github/gh-aw/error-messagesWrites clear validation error messages for gh-aw with actionable fixes
- Solves inconsistent or unclear validation error messages in gh-aw
- Uses NewValidationError and fmt.Errorf for structured error handling
- Follows a template with problem, expectation, and example in each message
- Delivers messages directly to users through the gh-aw CLI interface
SKILL.md
.github/skills/error-messagesView on GitHub ↗
---
name: error-messages
description: Write consistent, actionable validation error messages in gh-aw.
---
# Error Message Style Guide
Use this format for gh-aw validation errors. Keep messages clear, actionable, and example-driven.
## Error Message Template
```
[what's wrong]. [what's expected]. [example of correct usage]
```
Make each error message answer three questions:
1. **What's wrong?** - Clearly state the validation error
2. **What's expected?** - Explain the valid format or values
3. **How to fix it?** - Provide a concrete example of correct usage
## Constructive Language
Avoid standalone negative wording. Pair it with expected behavior and a concrete fix.
| Avoid only-negative wording | Prefer constructive wording |
|---|---|
| `invalid` | `expected` + valid format/options |
| `cannot` | `requires` + precondition |
| `must` | `should` + example |
| `failed` | action context + recovery step |
❌ `invalid repo format: %s`
✅ `invalid repo format '%s' — expected 'owner/repo' format (for example: 'github/gh-aw')`
❌ `not in a git repository`
✅ `not in a git repository — run 'git init' or 'cd' to a git repository`
## When to use `NewValidationError` vs `fmt.Errorf`
- Use `NewValidationError(field, value, reason, suggestion)` in `*_validation.go` logic.
- Use `field` for the exact config path
- Use `reason` for what failed
- Use `suggestion` for an actionable fix with an example
- Use `fmt.Errorf` for operational/wrapping errors (`%w`) where you are propagating a lower-level failure with context.
- Avoid generic wrappers like `fmt.Errorf("failed to X: %w", err)` unless you add recovery guidance.
## Suggestion Text Checklist
Every `suggestion` should:
1. Explain what to change
2. Include a minimal valid YAML/code example
3. Use ✓/✗ markers when ambiguity is likely
Example:
```text
Use one supported engine.
✓ Example:
engine: copilot
✗ Avoid:
engine: unknown
```
## YAML Example Guidelines
- Keep examples minimal and valid YAML
- Use real frontmatter field names
- Quote only when YAML requires it
- Prefer 2-space indentation
## Good Examples
These examples follow the template and provide actionable guidance:
### Time Delta Validation (from time_delta.go)
```go
return nil, fmt.Errorf("invalid time delta format: +%s. Expected format like +25h, +3d, +1w, +1mo, +1d12h30m", deltaStr)
```
✅ **Why it's good:**
- Clearly identifies the invalid input
- Lists multiple valid format examples
- Shows combined formats (+1d12h30m)
### Type Validation with Example
```go
return "", fmt.Errorf("manual-approval value must be a string, got %T. Example: manual-approval: \"production\"", val)
```
✅ **Why it's good:**
- Shows actual type received (%T)
- Provides concrete YAML example
- Uses proper YAML syntax with quotes
### Enum Validation with Options
```go
return fmt.Errorf("invalid engine: %s. Valid engines are: copilot, claude, codex, custom. Example: engine: copilot", engineID)
```
✅ **Why it's good:**
- Lists all valid options
- Provides simplest example
- Uses consistent formatting
### MCP Configuration
```go
return fmt.Errorf("tool '%s' mcp configuration must specify either 'command' or 'container'. Example:\ntools:\n %s:\n command: \"npx @my/tool\"", toolName, toolName)
```
✅ **Why it's good:**
- Explains mutual exclusivity
- Shows realistic tool name
- Formats multi-line YAML example
## Bad Examples
These examples lack clarity or actionable guidance:
### Too Vague
```go
return fmt.Errorf("invalid format")
```
❌ **Problems:**
- Doesn't specify what format is invalid
- Doesn't explain expected format
- No example provided
### Missing Example
```go
return fmt.Errorf("manual-approval value must be a string")
```
❌ **Problems:**
- States requirement but no example
- User doesn't know proper YAML syntax
- Could be clearer about type received
### Incomplete Information
```go
return fmt.Errorf("invalid engine: %s", engineID)
```
❌ **Problems:**
- Doesn't list valid options
- No guidance on fixing the error
- User must search documentation
## When to Include Examples
Always include examples for:
1. **Format/Syntax Errors** - Show the correct syntax
```go
fmt.Errorf("invalid date format. Expected: YYYY-MM-DD HH:MM:SS. Example: 2024-01-15 14:30:00")
```
2. **Enum/Choice Fields** - List all valid options
```go
fmt.Errorf("invalid permission level: %s. Valid levels: read, write, none. Example: permissions:\n contents: read", level)
```
3. **Type Mismatches** - Show expected type and example
```go
fmt.Errorf("timeout-minutes must be an integer, got %T. Example: timeout-minutes: 10", value)
```
4. **Complex Configurations** - Provide complete valid example
```go
fmt.Errorf("invalid MCP server config. Example:\nmcp-servers:\n my-server:\n command: \"node\"\n args: [\"server.js\"]")
```
## When Examples May Be Optional
Examples can be omitted when:
1. **Error is from wrapped error** - When wrapping another error with context
```go
return fmt.Errorf("failed to parse configuration: %w", err)
```
2. **Error is self-explanatory with clear context**
```go
return fmt.Errorf("duplicate unit '%s' in time delta: +%s", unit, deltaStr)
```
3. **Error points to specific documentation**
```go
return fmt.Errorf("unsupported feature. See https://docs.example.com/features")
```
## Formatting Guidelines
### Use Type Verbs for Dynamic Content
- `%s` - strings
- `%d` - integers
- `%T` - type of value
- `%v` - general value
- `%w` - wrapped errors
### Multi-line Examples
For YAML configuration examples spanning multiple lines:
```go
fmt.Errorf("invalid config. Example:\ntools:\n github:\n mode: \"remote\"")
```
### Quoting in Examples
Use proper YAML syntax in examples:
```go
// Good - shows quotes when needed
fmt.Errorf("Example: name: \"my-workflow\"")
// Good - shows no quotes for simple values
fmt.Errorf("Example: timeout-minutes: 10")
```
### Consistent Terminology
Use the same field names as in YAML:
```go
// Good - matches YAML field name
fmt.Errorf("timeout-minutes must be positive")
// Bad - uses different name
fmt.Errorf("timeout must be positive")
```
## Error Message Testing
All improved error messages should have corresponding tests:
```go
func TestErrorMessageQuality(t *testing.T) {
err := validateSomething(invalidInput)
require.Error(t, err)
// Error should explain what's wrong
assert.Contains(t, err.Error(), "invalid")
// Error should include expected format or values
assert.Contains(t, err.Error(), "Expected")
// Error should include example
assert.Contains(t, err.Error(), "Example:")
}
```
## Migration Strategy
When improving existing error messages:
1. **Identify the error** - Find validation error that lacks clarity
2. **Analyze context** - Understand what's being validated
3. **Apply template** - Add what's wrong + expected + example
4. **Add tests** - Verify error message content
5. **Update comments** - Document the validation logic
## Examples by Category
### Format Validation
```go
// Time deltas
fmt.Errorf("invalid time delta format: +%s. Expected format like +25h, +3d, +1w, +1mo, +1d12h30m", input)
// Dates
fmt.Errorf("invalid date format: %s. Expected: YYYY-MM-DD or relative like -1w. Example: 2024-01-15 or -7d", input)
// URLs
fmt.Errorf("invalid URL format: %s. Expected: https:// URL. Example: https://api.example.com", input)
```
### Type Validation
```go
// Boolean expected
fmt.Errorf("read-only must be a boolean, got %T. Example: read-only: true", value)
// String expected
fmt.Errorf("workflow name must be a string, got %T. Example: name: \"my-workflow\"", value)
// Object expected
fmt.Errorf("permissions must be an object, got %T. Example: permissions:\n contents: read", value)
```
### Choice/Enum Validation
```go
// Engine selection
fmt.Errorf("invalid engine: %s. Valid engines: copilot, claude, codex, custom. Example: engine: copilot", id)
// Permission levels
fmt.Errorf("invalid permission level: %s. Valid levels: read, write, none. Example: contents: read", level)
// Tool modes
fmt.Errorf("invalid mode: %s. Valid modes: local, remote. Example: mode: \"remote\"", mode)
```
### Configuration Validation
```go
// Missing required field
fmt.Errorf("tool '%s' missing required 'command' field. Example:\ntools:\n %s:\n command: \"node server.js\"", name, name)
// Mutually exclusive fields
fmt.Errorf("cannot specify both 'command' and 'container'. Choose one. Example: command: \"node server.js\"")
// Invalid combination
fmt.Errorf("http MCP servers cannot use 'container' field. Example:\ntools:\n my-http:\n type: http\n url: \"https://api.example.com\"")
```
## References
- **Excellent example to follow**: `pkg/workflow/time_delta.go`
- **Pattern inspiration**: Go standard library error messages
- **Testing examples**: `pkg/workflow/*_test.go`
## Tools
When writing error messages, consider:
- The user's perspective (what do they need to fix it?)
- The context (where in the workflow is the error?)
- The documentation (should we reference specific docs?)
- The complexity (is multi-line example needed?)