Exceptions
Exception Handling¶
SRF defines two exception classes and catches some ORM, Pydantic, and HTTP exceptions within BaseViewSet.as_view(). It does not install a global unified exception handler.
SRF Exceptions¶
TargetObjectAlreadyExist: Subclass ofHTTPException, status code 409.ImproperlyConfigured: Subclass ofHTTPException, status code 500.
They only define status codes and descriptions, without custom JSON response structure. The final response format depends on where the exception occurs: inside ViewSet handler it goes through SRF's catching logic, while other routes are handled by Sanic.
ViewSet Built-in Handling¶
BaseViewSet.as_view() handles the following when calling the handler:
| Exception | Response |
|---|---|
tortoise.exceptions.DoesNotExist |
404 text response |
pydantic.ValidationError |
422 JSON: {"detail": str(error)} |
sanic.exceptions.HTTPException |
Exception status code, JSON: {"detail": ...} |
For example:
from sanic.exceptions import BadRequest
class ProductViewSet(BaseViewSet):
async def create(self, request):
if request.json is None:
raise BadRequest("Request body cannot be empty")
# ...
Permission checks happen before this try block, so Forbidden raised by check_permissions() is handled by Sanic, not going through the above JSON conversion.
Special Behavior for CRUD¶
get_object()usesget_or_none(), throwing SanicNotFoundif the object doesn't exist.perform_create()converts TortoiseIntegrityErrorto 409HTTPException(detail="data conflict").- When the request JSON is
None, built-in create/update directly returns an empty 400 response.
Global Unified Response¶
If your application needs consistent formatting, register a Sanic exception handler:
from sanic.exceptions import HTTPException
from sanic.response import json
@app.exception(HTTPException)
async def handle_http_exception(request, exception):
return json(
{
"error": type(exception).__name__,
"message": getattr(exception, "message", str(exception)),
},
status=exception.status_code,
)
Note: Exceptions already caught and converted by ViewSet will not reach this global handler. To achieve full uniformity across all endpoints, you need to adjust the catching strategy in BaseViewSet.as_view() as well.
In production environment, you can add a fallback handler, but do not expose stack traces to clients:
from sanic.log import error_logger
@app.exception(Exception)
async def handle_unexpected_error(request, exception):
error_logger.exception("Unhandled exception")
return json(
{"error": "INTERNAL_ERROR", "message": "Server internal error"},
status=500,
)
Pydantic Errors¶
The current built-in response uses str(error). If the client needs structured field errors, you can return error.errors() in a custom ViewSet handler, or modify the framework's catching logic:
except ValidationError as error:
return JSONResponse(
{"errors": error.errors(include_url=False)},
status=422,
)
Best Practices¶
- Categorize Exceptions: Use different exception classes for different types of errors
- Friendly Error Messages: Provide clear and helpful error messages
- Consistent Response Format: Use a consistent error response structure
- Log Detailed Context: Record context information for errors
- Hide Internal Details: Do not expose internal errors in production
- Use Appropriate Status Codes: Use correct HTTP status codes for different errors
- Internationalization: Support multilingual error messages
Next Steps¶
- Learn HTTP Status Codes to understand their usage
- Read Authentication to understand authentication exceptions
- View Views to understand exception handling in ViewSet