Error Handling¶
This guide explains how to handle errors effectively in your Pyvider provider.
Overview¶
Pyvider provides a comprehensive exception hierarchy for handling errors. Proper error handling ensures users receive clear, actionable error messages.
Available Exceptions¶
Pyvider's exception hierarchy:
PyviderError (base)
├── ConversionError
├── FrameworkConfigurationError
├── PluginError
├── PyviderValueError
├── InvalidTypeError
├── UnsupportedTypeError
├── ComponentConfigurationError
├── FunctionError
│ ├── FunctionRegistrationError
│ └── FunctionValidationError
├── GRPCError
├── ProviderError
│ ├── ProviderConfigurationError
│ └── ProviderInitializationError
├── ComponentRegistryError
├── ValidatorRegistrationError
├── ResourceError
│ ├── ResourceValidationError
│ ├── ResourceNotFoundError
│ ├── ResourceOperationError
│ └── ResourceLifecycleContractError
├── DataSourceError
├── CapabilityError
├── SchemaError
│ ├── SchemaValidationError
│ ├── SchemaRegistrationError
│ ├── SchemaParseError
│ └── SchemaConversionError
├── SerializationError
├── DeserializationError
├── ValidationError
└── AttributeValidationError
Basic Error Handling¶
Raising Exceptions¶
from pyvider.exceptions import ResourceError, ProviderConfigurationError
async def create_server(config):
if not config.name:
raise ResourceError("Server name is required")
try:
server = await api.create_server(config.name)
return server
except APIError as e:
raise ResourceError(f"Failed to create server: {e}")
Provider Configuration Errors¶
from pyvider.providers import BaseProvider
from pyvider.exceptions import ProviderConfigurationError
class MyProvider(BaseProvider):
async def configure(self, config: dict) -> None:
if not config.get("api_key"):
raise ProviderConfigurationError("api_key is required")
if not config["api_key"].startswith("sk_"):
raise ProviderConfigurationError("Invalid API key format")
try:
self.client = APIClient(config["api_key"])
await self.client.test_connection()
except ConnectionError as e:
raise ProviderConfigurationError(
f"Failed to connect to API: {e}"
)
Resource Errors¶
from pyvider.resources import BaseResource
from pyvider.resources.context import ResourceContext
from pyvider.exceptions import ResourceError, ResourceNotFoundError
class Server(BaseResource):
async def _create_apply(self, ctx: ResourceContext):
if not ctx.config:
return None, None
try:
server = await self.api.create_server(ctx.config.name)
except QuotaExceeded as e:
raise ResourceError(
f"Cannot create server: quota exceeded. "
f"Current: {e.current}, Limit: {e.limit}"
)
except AlreadyExists:
raise ResourceError(
f"Server '{ctx.config.name}' already exists. "
f"Import it with: terraform import mycloud_server.name <id>"
)
except APIError as e:
raise ResourceError(f"API error: {e}")
return ServerState(...), None
async def read(self, ctx: ResourceContext):
try:
server = await self.api.get_server(ctx.state.id)
return ServerState(...)
except NotFound:
return None # Resource deleted outside Terraform
except APIError as e:
raise ResourceError(f"Failed to read server: {e}")
Best Practices¶
1. Provide Actionable Error Messages¶
# Bad: Vague error
raise ResourceError("Creation failed")
# Good: Specific error with solution
raise ResourceError(
f"Cannot create server '{name}': quota exceeded. "
f"Current usage: {current}/{limit}. "
f"Request quota increase at https://console.example.com/quotas"
)
2. Include Context¶
# Bad: Missing context
raise ResourceError("Invalid size")
# Good: Clear context
raise ResourceError(
f"Invalid server size '{config.size}'. "
f"Valid sizes: small, medium, large"
)
3. Wrap External Errors¶
try:
result = await external_api.call()
except ExternalAPIError as e:
raise ResourceError(
f"External API call failed: {e}",
details={"api_error_code": e.code, "retry_after": e.retry_after}
)
4. Handle Validation Errors¶
async def _validate_config(self, config: ServerConfig) -> list[str]:
"""Validate configuration and return error list."""
errors = []
if not config.name:
errors.append("name is required")
if not 1 <= config.port <= 65535:
errors.append(f"port must be 1-65535, got {config.port}")
if config.memory_gb < 1:
errors.append(f"memory_gb must be >= 1, got {config.memory_gb}")
return errors
5. Log Before Raising¶
from provide.foundation import logger
async def create_resource(config):
try:
resource = await api.create(config)
return resource
except APIError as e:
logger.error("Resource creation failed",
error_type=type(e).__name__,
error_message=str(e),
resource_name=config.name
)
raise ResourceError(f"Failed to create resource: {e}")
Error Patterns¶
Quota Exceeded¶
except QuotaExceeded as e:
raise ResourceError(
f"Cannot create {resource_type}: quota exceeded.\n"
f"Current: {e.current_usage}\n"
f"Limit: {e.quota_limit}\n"
f"Request increase at: {e.quota_url}"
)
Resource Not Found¶
async def read(self, ctx: ResourceContext):
try:
return await self.api.get_resource(ctx.state.id)
except NotFoundError:
# Return None - Terraform will recreate
return None
Conflict/Already Exists¶
except ConflictError as e:
raise ResourceError(
f"Resource '{config.name}' already exists.\n"
f"To manage it with Terraform, import it:\n"
f" terraform import mycloud_server.name {e.existing_id}"
)
Permission Denied¶
except PermissionDenied as e:
raise ResourceError(
f"Permission denied: {e.message}\n"
f"Required permissions: {', '.join(e.required_perms)}\n"
f"Current permissions: {', '.join(e.current_perms)}"
)
Temporary Failures¶
max_retries = 3
for attempt in range(max_retries):
try:
return await api.create_resource(config)
except TemporaryError as e:
if attempt == max_retries - 1:
raise ResourceError(
f"Resource creation failed after {max_retries} attempts: {e}"
)
await asyncio.sleep(2 ** attempt) # Exponential backoff
Testing Error Scenarios¶
import pytest
from pyvider.exceptions import ResourceError
@pytest.mark.asyncio
async def test_create_server_quota_exceeded(server_resource, mock_api):
# Arrange
mock_api.create_server.side_effect = QuotaExceeded(current=10, limit=10)
# Act & Assert
with pytest.raises(ResourceError) as exc_info:
await server_resource._create_apply(ctx)
assert "quota exceeded" in str(exc_info.value).lower()
assert "10" in str(exc_info.value) # Shows limits
Common Issues¶
Missing Resource ID¶
Partial Update Failures¶
async def _update_apply(self, ctx: ResourceContext):
updated_fields = []
try:
if ctx.config.name != ctx.state.name:
await api.update_name(ctx.state.id, ctx.config.name)
updated_fields.append("name")
if ctx.config.size != ctx.state.size:
await api.update_size(ctx.state.id, ctx.config.size)
updated_fields.append("size")
return await self.read(ctx)
except APIError as e:
raise ResourceError(
f"Update failed after updating: {', '.join(updated_fields)}. "
f"Error: {e}. "
f"Resource may be in inconsistent state."
)
Cascading Failures¶
async def _delete_apply(self, ctx: ResourceContext):
errors = []
# Try to delete dependent resources
for dep in ctx.state.dependencies:
try:
await api.delete_dependency(dep.id)
except APIError as e:
errors.append(f"Failed to delete {dep.type} {dep.id}: {e}")
# Delete main resource
try:
await api.delete_resource(ctx.state.id)
except APIError as e:
errors.append(f"Failed to delete resource: {e}")
if errors:
raise ResourceError(
f"Deletion completed with errors:\n" +
"\n".join(f" - {err}" for err in errors)
)
See Also¶
- Logging - Logging best practices
- Debugging - Debugging techniques
- Best Practices - Provider development patterns
- API Reference - Complete exception reference