Azure Developer CLI includes an extensible error handling pipeline that transforms cryptic error messages into user-friendly guidance. When users encounter well-known errors (quota limits, authentication failures, deployment conflicts, missing tools, etc.), azd displays:
- A user-friendly message explaining what went wrong
- An actionable suggestion for next steps
- A documentation link for more information
- The original error (in grey) for technical reference
┌─────────────────────────────────────────────────────────────────┐
│ Error Occurs │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ ErrorHandlerPipeline evaluates rules from │
│ resources/error_suggestions.yaml │
│ │
│ For each rule: │
│ 1. errorType? → Match Go error type via reflection │
│ 2. properties? → Check struct fields via dot-path │
│ 3. patterns? → Match error text (substring/regex) │
│ 4. handler? → Invoke named handler for dynamic response │
│ │
│ All specified conditions must pass. First matching rule wins. │
└─────────────────────────────────────────────────────────────────┘
│
┌───────────────┴───────────────┐
│ │
▼ ▼
┌─────────────────────────┐ ┌─────────────────────────────────┐
│ Rule Matched │ │ No Match │
│ Wrap error with │ │ Return original error │
│ ErrorWithSuggestion │ │ (may go to AI if enabled) │
└─────────────────────────┘ └─────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ UxMiddleware displays: │
│ 1. User-friendly message (ERROR: ...) │
│ 2. Actionable suggestion (Suggestion: ...) │
│ 3. Documentation link (Learn more: ...) │
│ 4. Original error in grey (technical details) │
└─────────────────────────────────────────────────────────────────┘
When a user hits a quota error, instead of seeing a wall of red text, they see:
ERROR: Your Azure subscription has reached a resource quota limit.
Suggestion: Request a quota increase through the Azure portal, or try deploying to a different region.
Learn more: https://learn.microsoft.com/azure/quotas/quickstart-increase-quota-portal
Deployment failed: QuotaExceeded for resource type Microsoft.Compute/virtualMachines in location eastus...
The error rules are defined in resources/error_suggestions.yaml. This file is designed to be easily editable by anyone—no Go programming knowledge required for most cases.
Match against the error message text. Good for tool errors, CLI messages, and any error without a typed Go struct.
- patterns:
- "quota exceeded" # Case-insensitive substring
- "QuotaExceeded"
message: "Your Azure subscription has reached a resource quota limit."
suggestion: "Request a quota increase through the Azure portal."
docUrl: "https://learn.microsoft.com/azure/quotas/..."Match against specific Go error types using reflection. This lets you target structured errors and inspect their fields. For ARM deployment errors, use DeploymentErrorLine to match error codes at any depth in the error tree.
# Match ARM deployment errors with a specific error code
# DeploymentErrorLine nodes are found at any depth via multi-unwrap
- errorType: "DeploymentErrorLine"
properties:
Code: "FlagMustBeSetForRestore"
message: "A soft-deleted resource is blocking deployment."
suggestion: "Run 'azd down --purge' to permanently remove it, then retry."
docUrl: "https://learn.microsoft.com/azure/key-vault/general/key-vault-recovery"
# Match auth errors (direct type match)
- errorType: "AuthFailedError"
message: "Authentication with Azure failed."
suggestion: "Run 'azd auth login' to sign in again."How it works:
errorTypeis the Go struct type name (e.g.,DeploymentErrorLine,ExitError)- The error chain is walked (including multi-unwrap trees) to find the matching type
propertiesuses dot notation to access struct fields via reflection (e.g.,Code)- Both type AND properties must match on the same error node
- By default, patterns and property values use case-insensitive substring matching
- Set
regex: trueon the rule to treat all patterns and property values as regular expressions
When error codes are too broad (like generic "Conflict"), combine type matching with text patterns to narrow the match:
- errorType: "DeploymentErrorLine"
regex: true
properties:
Code: "Conflict"
patterns:
- "(?i)soft.?delete" # Also require this text in the error message
message: "A soft-deleted resource is causing a deployment conflict."
suggestion: "Purge the resource in the Azure portal, then retry."For cases that need code to compute a suggestion (e.g., querying Azure for available regions), you can reference a named ErrorHandler registered in the IoC container:
- errorType: "DeploymentErrorLine"
properties:
Code: "SkuNotAvailable"
handler: "resourceNotAvailableHandler"When a handler is set, the static message/suggestion/docUrl fields are ignored — the handler computes the full response dynamically.
The handler implements the ErrorHandler interface:
// pkg/errorhandler/handler.go
type ErrorHandler interface {
Handle(ctx context.Context, err error) *ErrorWithSuggestion
}Example — the built-in ResourceNotAvailableHandler extracts the ARM resource type from the error message, queries the Azure Providers API for available regions, and builds a targeted suggestion:
func (h *ResourceNotAvailableHandler) Handle(ctx context.Context, err error) *ErrorWithSuggestion {
location := os.Getenv("AZURE_LOCATION")
subscriptionID := os.Getenv("AZURE_SUBSCRIPTION_ID")
resourceType := extractResourceType(err.Error()) // e.g. "Microsoft.Web/staticSites"
var availableLocations []string
if resourceType != "" && subscriptionID != "" && h.locationResolver != nil {
availableLocations, _ = h.locationResolver.GetLocations(ctx, subscriptionID, resourceType)
}
return h.buildSuggestion(err, location, resourceType, availableLocations)
}Handlers that need external dependencies use interfaces to avoid import cycles. The ResourceNotAvailableHandler accepts a ResourceTypeLocationResolver interface:
type ResourceTypeLocationResolver interface {
GetLocations(ctx context.Context, subscriptionID string, resourceType string) ([]string, error)
}The concrete implementation lives in pkg/azapi/resource_type_locations.go following the package's service conventions, and is wired in cmd/container.go:
// pkg/azapi/resource_type_locations.go
func NewResourceTypeLocationService(
credentialProvider account.SubscriptionCredentialProvider,
armClientOptions *arm.ClientOptions,
) *ResourceTypeLocationService { ... }
// cmd/container.go
container.MustRegisterSingleton(azapi.NewResourceTypeLocationService)
container.MustRegisterNamedSingleton("resourceNotAvailableHandler",
func(locationService *azapi.ResourceTypeLocationService) errorhandler.ErrorHandler {
return errorhandler.NewResourceNotAvailableHandler(locationService)
},
)| Field | Required | Description |
|---|---|---|
patterns |
At least one of patterns or errorType |
List of strings/regex to match against error text |
errorType |
At least one of patterns or errorType |
Go error struct type name (matched via reflection) |
properties |
No (requires errorType) |
Map of dot-path field names to expected values |
regex |
No | When true, all patterns and property values use regex matching |
message |
Yes (unless handler is set) |
User-friendly explanation of what went wrong |
suggestion |
Yes (unless handler is set) |
Actionable next steps for the user |
docUrl |
No | Link to relevant documentation |
handler |
No | Name of IoC-registered ErrorHandler for dynamic suggestions |
Case-insensitive substring matching:
patterns:
- "quota exceeded" # Matches "QuotaExceeded", "QUOTA EXCEEDED", etc.Set regex: true on the rule to treat all patterns and property values as regular expressions:
regex: true
patterns:
- "(?i)authorization.*failed"
- "BCP\\d{3}"| Pattern | Meaning |
|---|---|
(?i) |
Case-insensitive |
.* |
Match any characters |
\\d+ |
One or more digits |
| `(foo | bar)` |
Note: In YAML, backslashes must be escaped as \\.
- First match wins: Rules are evaluated in order from top to bottom
- All conditions must pass: If a rule has both
errorTypeandpatterns, both must match - Order matters: Place more specific rules before general ones
-
Keep messages simple: Explain what went wrong in plain language
message: "Your Azure subscription has reached a resource quota limit."
-
Make suggestions actionable: Tell users exactly what to do
suggestion: "Run 'azd auth login' to sign in again."
-
Use
errorTypefor structured errors: When the error has a known Go type, prefer type matching over text patterns — it's more reliable and less fragile -
Order by specificity: Place more specific rules before general ones. For example,
Conflict + soft-delete keywordrules must come before the bareConflictrule. Text-only patterns should come last since they're the broadest. -
Test your rules: Run
go test ./pkg/errorhandler/...after making changes
| File | Purpose |
|---|---|
resources/error_suggestions.yaml |
Error rules (edit this!) |
pkg/errorhandler/types.go |
YAML schema types |
pkg/errorhandler/pipeline.go |
Rule evaluation pipeline |
pkg/errorhandler/reflect.go |
Reflection-based error type/property matching (supports multi-unwrap) |
pkg/errorhandler/matcher.go |
Text pattern matching engine |
pkg/errorhandler/handler.go |
ErrorHandler interface for custom handlers |
pkg/errorhandler/resource_availability_handler.go |
ResourceNotAvailableHandler — queries ARM for available regions |
pkg/azapi/resource_type_locations.go |
ARM SDK implementation: queries available regions per resource type |
pkg/errorhandler/errors.go |
ErrorWithSuggestion type |
pkg/output/ux/error_with_suggestion.go |
UX display component |
The pipeline is the single entry point. It evaluates YAML rules in order, checking each rule's conditions (errorType, properties, patterns). When all conditions pass, it either returns a static suggestion from the rule's fields or invokes a named handler.
The error type matcher uses depth-first traversal with multi-unwrap support (Unwrap() []error), so it can find typed errors at any depth in an error tree — including nested ARM deployment errors.
The canonical error type lives in pkg/errorhandler so extensions can also create and return user-friendly errors:
type ErrorWithSuggestion struct {
Err error // Original error
Message string // User-friendly explanation
Suggestion string // Actionable next steps
DocUrl string // Optional documentation link
}A type alias in internal/ provides backward compatibility for existing code.
Extensions can participate in error handling by:
- Returning
ErrorWithSuggestion: Extensions can directly wrap errors with suggestions - Registering named handlers: Extensions can register
ErrorHandlerimplementations via IoC for dynamic suggestion computation