Skip to main content

Response and Exception Handling Guide

Overview

Taruvi uses a standardized approach for handling responses and exceptions across all API endpoints.

Error Handling

Use raise with exception classes:

from ninja import Router
from base.responses.exceptions import (
ValidationException,
ResourceNotFoundException,
PermissionException,
ConflictException,
AppException
)

router = Router()

@router.get("/items/{id}/")
def get_item(request, id: int):
# Validation error (400)
if id < 0:
raise ValidationException(
message="Invalid ID",
detail="ID must be a positive integer"
)

# Not found error (404)
try:
item = Item.objects.get(id=id)
except Item.DoesNotExist:
raise ResourceNotFoundException(
message="Item not found",
detail=f"No item exists with ID {id}"
)

# Permission error (403)
if not request.user.has_perm('view_item'):
raise PermissionException(
message="Permission denied",
detail="You don't have permission to view this item"
)

# Success response
return 200, SuccessDataResponse(
message="Item retrieved successfully",
data=item
)

Success Responses

Use tuple returns with response schemas:

from base.responses import SuccessDataResponse, SuccessResponse

# Data response
return 200, SuccessDataResponse(
message="Items retrieved successfully",
data=items,
total=count
)

# Simple message response
return 200, SuccessResponse(
message="Operation completed successfully"
)

# Created response
return 201, SuccessDataResponse(
message="Item created successfully",
data=new_item
)

# No content (DELETE)
return 204, None

Exception Classes

All exceptions inherit from AppException and automatically map to HTTP status codes.

Exception ClassHTTP StatusError CodeUse Case
ValidationException400VALIDATION_ERRORInvalid input data
AuthenticationException401AUTHENTICATION_FAILEDAuthentication required/failed
PermissionException403PERMISSION_DENIEDInsufficient permissions
ResourceNotFoundException404NOT_FOUNDResource doesn't exist
ConflictException409CONFLICTResource conflict
DuplicateEntryException409DUPLICATE_ENTRYDuplicate resource
BusinessLogicException422BUSINESS_LOGIC_ERRORBusiness rule violation
RateLimitException429RATE_LIMIT_EXCEEDEDToo many requests
ServiceUnavailableException503SERVICE_UNAVAILABLEService unavailable
GatewayTimeoutException504GATEWAY_TIMEOUTRequest timeout
AppException500INTERNAL_ERRORGeneric server error

Exception Parameters

All exceptions accept these parameters:

raise ValidationException(
message="Human-readable error message", # Required
detail="Additional context or technical details", # Optional
errors={"field": ["Error 1", "Error 2"]}, # Optional - field-level errors
data={"key": "value"} # Optional - additional error context
)

Response Formats

Success Response Format

{
"status": "success",
"message": "Operation completed successfully",
"data": {...},
"total": 100
}

Error Response Format

Exceptions are automatically converted to this format:

{
"status": "error",
"code": "VALIDATION_ERROR",
"message": "Validation failed",
"detail": "Additional error context",
"errors": {
"field_name": ["Error message"]
}
}

Best Practices

✅ DO

# Use specific exception classes
raise ValidationException(message="Invalid email format")

# Provide helpful error messages
raise ResourceNotFoundException(
message="User not found",
detail=f"No user exists with ID {user_id}"
)

# Use tuple returns for success
return 200, SuccessDataResponse(message="Success", data=result)

❌ DON'T

# Don't return raw dicts
return {"error": "Something went wrong"} # ❌

# Don't use generic exceptions without context
raise AppException() # ❌ - No message

# Don't return error tuples
return 400, {"error": "Invalid"} # ❌ - Use raise instead

Common Patterns

Validation Errors

# Single validation error
raise ValidationException(
message="Invalid input",
detail="Email format is invalid"
)

# Multiple field errors
raise ValidationException(
message="Validation failed",
errors={
"email": ["Invalid format"],
"age": ["Must be at least 18"]
}
)

Not Found Errors

try:
obj = Model.objects.get(id=obj_id)
except Model.DoesNotExist:
raise ResourceNotFoundException(
message=f"{Model.__name__} not found",
detail=f"No {Model.__name__} exists with ID {obj_id}"
)

Permission Errors

if not request.user.has_perm('app.permission'):
raise PermissionException(
message="Permission denied",
detail="You need 'permission' to perform this action"
)

Conflict Errors

if Model.objects.filter(email=email).exists():
raise DuplicateEntryException(
message="Email already exists",
detail=f"A user with email '{email}' already exists"
)

Testing

Testing Exception Handling

def test_validation_error():
with pytest.raises(ValidationException) as exc_info:
validate_data(invalid_data)

assert exc_info.value.message == "Validation failed"
assert exc_info.value.code == ErrorCode.VALIDATION_ERROR

Testing API Responses

def test_api_error_response(client):
response = client.get('/api/items/999/')

assert response.status_code == 404
assert response.json()['status'] == 'error'
assert response.json()['code'] == 'NOT_FOUND'
assert 'message' in response.json()

Summary

  • Use raise for errors with specific exception classes
  • Use tuple returns for success responses
  • Always provide meaningful error messages and details
  • Let the exception handler convert exceptions to proper responses

For more examples, see:

  • cloud_site/data/api/routers/data.py - API examples
  • base/responses/exceptions.py - Exception classes