Error Handling
Global error codes, response formats, and debugging strategies for ObjectStack APIs
Error Handling
The Error Handling Protocol defines standardized error codes, response formats, and debugging strategies across all ObjectStack APIs (HTTP, WebSocket, GraphQL).
Why Standardized Errors Matter
Problem: Traditional APIs return errors inconsistently:
// Different error structures across endpoints 😱
API 1: { error: "Not found" }
API 2: { errors: [{ message: "Not found" }] }
API 3: { success: false, msg: "Not found", code: 404 }
API 4: HTTP 200 OK with { status: "error", ... }Developer pain:
- Every endpoint requires custom error handling logic
- Difficult to show user-friendly messages
- Debugging is nightmare (where did this error originate?)
- Monitoring/alerting is inconsistent
Solution: ObjectStack enforces a single error format across all communication channels. Every error has a machine-readable code, human-readable message, and context for debugging.
Business Value Delivered
Better User Experience
Consistent error messages guide users to resolution. No generic 'Something went wrong' nonsense.
Faster Debugging
Error codes + request IDs = find root cause in seconds. Save hours of log hunting.
Automated Monitoring
Alert on specific error codes (e.g., RATE_LIMITED spikes = upgrade prompts working).
Security Hardening
Never leak sensitive info in errors. Attackers can't probe your system via error messages.
Standard Error Response
Every error follows this structure:
{
"success": false,
"error": {
"code": "ERROR_CODE",
"message": "Human-readable description",
"details": { /* Additional context */ },
"request_id": "req_abc123",
"timestamp": "2024-01-16T14:30:00Z"
}
}Fields:
success: Alwaysfalsefor errorserror.code: Machine-readable error code (use for conditionals)error.message: Human-readable message (show to users or developers)error.details: Additional context (field names, constraints, etc.)error.request_id: Unique request identifier for debuggingerror.timestamp: When error occurred (ISO 8601)
HTTP Status Codes
ObjectStack uses standard HTTP status codes:
| Status | Meaning | When Used |
|---|---|---|
| 400 | Bad Request | Invalid input, validation failure |
| 401 | Unauthorized | Authentication required or failed |
| 403 | Forbidden | Authenticated but insufficient permissions |
| 404 | Not Found | Resource doesn't exist |
| 409 | Conflict | Resource already exists or version mismatch |
| 422 | Unprocessable Entity | Business logic validation failed |
| 429 | Too Many Requests | Rate limit exceeded |
| 500 | Internal Server Error | Server-side error |
| 503 | Service Unavailable | Server overloaded or maintenance |
Important: Even on error, response body always includes JSON error object.
Error Codes
Authentication & Authorization
UNAUTHORIZED
HTTP Status: 401
Meaning: No authentication credentials provided or invalid credentials
Example:
{
"success": false,
"error": {
"code": "UNAUTHORIZED",
"message": "Authentication required",
"details": {
"hint": "Include 'Authorization: Bearer <token>' header"
}
}
}How to fix:
- Include valid JWT token in
Authorizationheader - Refresh expired tokens
- Re-authenticate user
INVALID_TOKEN
HTTP Status: 401
Meaning: Token is malformed or invalid
Example:
{
"success": false,
"error": {
"code": "INVALID_TOKEN",
"message": "JWT token is invalid",
"details": {
"reason": "signature_verification_failed"
}
}
}How to fix:
- Check token hasn't been tampered with
- Verify token is meant for this API (check
audclaim) - Ensure server secret key is correct
TOKEN_EXPIRED
HTTP Status: 401
Meaning: JWT token has expired
Example:
{
"success": false,
"error": {
"code": "TOKEN_EXPIRED",
"message": "JWT token expired",
"details": {
"expired_at": "2024-01-16T10:00:00Z",
"current_time": "2024-01-16T14:30:00Z"
}
}
}How to fix:
- Refresh token using refresh token flow
- Re-authenticate user
- Check token lifetime settings (typically 15-60 minutes)
FORBIDDEN
HTTP Status: 403
Meaning: Authenticated but insufficient permissions
Example:
{
"success": false,
"error": {
"code": "FORBIDDEN",
"message": "Insufficient permissions to access this resource",
"details": {
"required_permission": "account:write",
"user_permissions": ["account:read"]
}
}
}How to fix:
- Request permission from administrator
- Check row-level security rules
- Verify user role assignments
Validation Errors
VALIDATION_ERROR
HTTP Status: 400
Meaning: Input validation failed (schema validation)
Example:
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Validation failed for 2 fields",
"details": {
"fields": [
{
"field": "email",
"message": "Invalid email format",
"constraint": "format",
"value": "not-an-email"
},
{
"field": "age",
"message": "Must be at least 18",
"constraint": "min",
"value": 15,
"expected": 18
}
]
}
}
}How to fix:
- Check field constraints in object schema (
GET /api/v1/meta/object/{object}) - Validate input client-side before submission
- Show field-specific errors in UI
Client-side handling:
if (error.code === 'VALIDATION_ERROR') {
error.details.fields.forEach(({ field, message }) => {
showFieldError(field, message);
});
}REQUIRED_FIELD
HTTP Status: 400
Meaning: Required field is missing
Example:
{
"success": false,
"error": {
"code": "REQUIRED_FIELD",
"message": "Missing required field: name",
"details": {
"field": "name",
"constraint": "required"
}
}
}INVALID_TYPE
HTTP Status: 400
Meaning: Field value has wrong type
Example:
{
"success": false,
"error": {
"code": "INVALID_TYPE",
"message": "Field 'age' must be a number",
"details": {
"field": "age",
"expected_type": "number",
"actual_type": "string",
"value": "twenty-five"
}
}
}Resource Errors
NOT_FOUND
HTTP Status: 404
Meaning: Requested resource doesn't exist
Example:
{
"success": false,
"error": {
"code": "NOT_FOUND",
"message": "Account with id 'acc_999' not found",
"details": {
"resource": "account",
"resource_id": "acc_999"
}
}
}How to fix:
- Verify resource ID is correct
- Check user has permission to see resource (row-level security)
- Resource may have been deleted
ALREADY_EXISTS
HTTP Status: 409
Meaning: Resource with unique constraint already exists
Example:
{
"success": false,
"error": {
"code": "ALREADY_EXISTS",
"message": "Account with email 'john@acme.com' already exists",
"details": {
"resource": "account",
"constraint": "unique",
"field": "email",
"value": "john@acme.com"
}
}
}How to fix:
- Check for existing resource before creating
- Update existing resource instead of creating new one
- Use different value for unique field
CONSTRAINT_VIOLATION
HTTP Status: 409
Meaning: Operation violates database constraint
Example:
{
"success": false,
"error": {
"code": "CONSTRAINT_VIOLATION",
"message": "Cannot delete account with active opportunities",
"details": {
"resource": "account",
"resource_id": "acc_123",
"constraint": "foreign_key",
"related_object": "opportunity",
"related_count": 5
}
}
}How to fix:
- Delete related records first
- Enable cascade delete on object schema
- Archive instead of delete (soft delete)
Rate Limiting
RATE_LIMITED
HTTP Status: 429
Meaning: Too many requests, rate limit exceeded
Example:
{
"success": false,
"error": {
"code": "RATE_LIMITED",
"message": "Rate limit exceeded",
"details": {
"limit": 1000,
"window": "1m",
"retry_after": 45,
"quota_reset": "2024-01-16T14:31:00Z"
}
}
}HTTP Headers:
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1705412460
Retry-After: 45How to fix:
- Implement exponential backoff
- Batch requests to reduce call count
- Upgrade to higher tier for increased limits
- Cache responses to avoid repeated calls
Client-side handling:
async function fetchWithRetry(url, options = {}, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
const response = await fetch(url, options);
if (response.status !== 429) {
return response;
}
const retryAfter = response.headers.get('Retry-After');
await sleep(retryAfter * 1000);
}
throw new Error('Max retries exceeded');
}QUOTA_EXCEEDED
HTTP Status: 429
Meaning: Monthly/daily quota exceeded
Example:
{
"success": false,
"error": {
"code": "QUOTA_EXCEEDED",
"message": "Monthly API quota exceeded",
"details": {
"quota": 10000,
"used": 10000,
"period": "monthly",
"reset": "2024-02-01T00:00:00Z",
"upgrade_url": "https://app.acme.com/billing/upgrade"
}
}
}How to fix:
- Wait for quota reset
- Upgrade to higher plan
- Optimize API usage
Business Logic Errors
BUSINESS_RULE_VIOLATION
HTTP Status: 422
Meaning: Operation violates business logic rule
Example:
{
"success": false,
"error": {
"code": "BUSINESS_RULE_VIOLATION",
"message": "Cannot close opportunity without selecting a stage reason",
"details": {
"rule": "opportunity_close_validation",
"field": "stage_reason",
"required_when": "stage === 'Closed Won' || stage === 'Closed Lost'"
}
}
}How to fix:
- Check validation rules in object schema
- Provide required fields
- Follow business process workflow
WORKFLOW_ERROR
HTTP Status: 422
Meaning: Workflow/automation failed
Example:
{
"success": false,
"error": {
"code": "WORKFLOW_ERROR",
"message": "Approval workflow rejected the request",
"details": {
"workflow": "purchase_order_approval",
"step": "manager_approval",
"reason": "Amount exceeds manager approval limit",
"limit": 10000,
"requested": 15000
}
}
}Server Errors
SERVER_ERROR
HTTP Status: 500
Meaning: Internal server error
Example:
{
"success": false,
"error": {
"code": "SERVER_ERROR",
"message": "An internal error occurred",
"details": {
"request_id": "req_abc123",
"support_url": "https://support.acme.com/request/req_abc123"
}
}
}Important: Never leak stack traces or sensitive internals to clients.
How to fix:
- Retry request (may be transient)
- Check server status page
- Contact support with
request_id
SERVICE_UNAVAILABLE
HTTP Status: 503
Meaning: Server temporarily unavailable
Example:
{
"success": false,
"error": {
"code": "SERVICE_UNAVAILABLE",
"message": "Service temporarily unavailable",
"details": {
"reason": "database_maintenance",
"retry_after": 300,
"estimated_completion": "2024-01-16T15:00:00Z"
}
}
}HTTP Headers:
HTTP/1.1 503 Service Unavailable
Retry-After: 300WebSocket-Specific Errors
CONNECTION_LIMIT_EXCEEDED
Meaning: Too many concurrent WebSocket connections
Example:
{
"type": "error",
"error": {
"code": "CONNECTION_LIMIT_EXCEEDED",
"message": "Maximum 5 concurrent connections per user",
"details": {
"limit": 5,
"current": 5
}
}
}How to fix:
- Close unused connections
- Implement connection pooling
- Use fewer browser tabs
SUBSCRIPTION_LIMIT_EXCEEDED
Meaning: Too many subscriptions per connection
Example:
{
"type": "error",
"subscription_id": "sub_99",
"error": {
"code": "SUBSCRIPTION_LIMIT_EXCEEDED",
"message": "Maximum 50 subscriptions per connection",
"details": {
"limit": 50,
"current": 50
}
}
}How to fix:
- Unsubscribe from unused subscriptions
- Use broader filters instead of many narrow subscriptions
- Combine related subscriptions
INVALID_MESSAGE
Meaning: WebSocket message is malformed
Example:
{
"type": "error",
"error": {
"code": "INVALID_MESSAGE",
"message": "Invalid JSON in WebSocket message",
"details": {
"reason": "unexpected_token",
"position": 42
}
}
}Error Response Examples
Validation Error (Multiple Fields)
Request:
POST /api/v1/data/account
Content-Type: application/json
{
"name": "",
"email": "not-an-email",
"revenue": -1000
}Response:
HTTP/1.1 400 Bad Request
Content-Type: application/json
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Validation failed for 3 fields",
"details": {
"fields": [
{
"field": "name",
"message": "Name is required",
"constraint": "required",
"value": ""
},
{
"field": "email",
"message": "Invalid email format",
"constraint": "format",
"value": "not-an-email"
},
{
"field": "revenue",
"message": "Revenue must be positive",
"constraint": "min",
"value": -1000,
"expected": 0
}
]
},
"request_id": "req_abc123",
"timestamp": "2024-01-16T14:30:00Z"
}
}Permission Denied
Request:
DELETE /api/v1/data/account/acc_123
Authorization: Bearer <token>Response:
HTTP/1.1 403 Forbidden
Content-Type: application/json
{
"success": false,
"error": {
"code": "FORBIDDEN",
"message": "Insufficient permissions to delete accounts",
"details": {
"resource": "account",
"resource_id": "acc_123",
"required_permission": "account:delete",
"user_permissions": ["account:read", "account:write"],
"hint": "Contact your administrator to request delete permission"
},
"request_id": "req_def456",
"timestamp": "2024-01-16T14:35:00Z"
}
}Rate Limit Exceeded
Request:
GET /api/v1/data/task
Authorization: Bearer <token>Response:
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1705412460
Retry-After: 45
Content-Type: application/json
{
"success": false,
"error": {
"code": "RATE_LIMITED",
"message": "Rate limit exceeded: 1000 requests per minute",
"details": {
"limit": 1000,
"window": "1m",
"retry_after": 45,
"quota_reset": "2024-01-16T14:31:00Z",
"upgrade_url": "https://app.acme.com/billing/upgrade"
},
"request_id": "req_ghi789",
"timestamp": "2024-01-16T14:30:15Z"
}
}Error Handling Best Practices
✅ Use Error Codes, Not Messages
Bad:
if (error.message.includes('not found')) {
// Brittle - breaks if message changes
}Good:
if (error.code === 'NOT_FOUND') {
// Reliable - code never changes
}✅ Show User-Friendly Messages
Bad:
alert(error.message); // "VALIDATION_ERROR: Field 'email' constraint 'format' failed"Good:
const userMessages = {
'VALIDATION_ERROR': 'Please check your input and try again',
'UNAUTHORIZED': 'Please log in to continue',
'RATE_LIMITED': 'Too many requests. Please wait a moment.',
};
showToast(userMessages[error.code] || 'An error occurred');✅ Handle Field-Specific Validation Errors
Good:
async function handleSubmit(data) {
try {
const response = await api.createAccount(data);
return response.data;
} catch (error) {
if (error.code === 'VALIDATION_ERROR') {
// Show errors next to fields
error.details.fields.forEach(({ field, message }) => {
setFieldError(field, message);
});
} else {
// Show general error
showToast(error.message, 'error');
}
throw error;
}
}✅ Implement Retry Logic
Good:
async function fetchWithRetry(url, options = {}, maxRetries = 3) {
let lastError;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const response = await fetch(url, options);
const data = await response.json();
if (!response.ok) {
// Check if error is retryable
if (data.error.code === 'RATE_LIMITED') {
const retryAfter = data.error.details.retry_after || 1;
await sleep(retryAfter * 1000);
continue;
} else if (data.error.code === 'SERVER_ERROR') {
// Exponential backoff
await sleep(Math.pow(2, attempt) * 1000);
continue;
} else {
// Don't retry validation errors, auth errors, etc.
throw new APIError(data.error);
}
}
return data;
} catch (error) {
lastError = error;
}
}
throw lastError;
}✅ Log Request IDs for Debugging
Good:
try {
await api.createAccount(data);
} catch (error) {
console.error('Account creation failed', {
request_id: error.request_id,
code: error.code,
message: error.message,
timestamp: error.timestamp
});
// Send to error tracking service
Sentry.captureException(error, {
extra: { request_id: error.request_id }
});
}✅ Handle Network Errors
Good:
try {
const response = await fetch('/api/v1/data/task');
const data = await response.json();
if (!data.success) {
throw new APIError(data.error);
}
return data.data;
} catch (error) {
if (error instanceof TypeError && error.message === 'Failed to fetch') {
// Network error - server unreachable
showToast('Network error. Please check your connection.', 'error');
} else if (error instanceof APIError) {
// API returned error
handleAPIError(error);
} else {
// Unknown error
console.error('Unexpected error:', error);
showToast('An unexpected error occurred', 'error');
}
}Debugging with Request IDs
Every API response includes a request_id for debugging:
Request:
POST /api/v1/data/account
Content-Type: application/json
X-Request-ID: my-custom-id-123
{ "name": "Acme Corp" }Response:
{
"success": true,
"data": { ... },
"request_id": "my-custom-id-123"
}Server logs:
[2024-01-16T14:30:00Z] INFO [my-custom-id-123] POST /api/v1/data/account
[2024-01-16T14:30:00Z] DEBUG [my-custom-id-123] Validating input
[2024-01-16T14:30:00Z] DEBUG [my-custom-id-123] Executing query: INSERT INTO accounts...
[2024-01-16T14:30:00Z] INFO [my-custom-id-123] Request completed in 45msUse request IDs to:
- Trace request through logs
- Debug distributed systems
- Report issues to support
- Correlate frontend errors with backend logs
Monitoring & Alerting
Error Rate Monitoring
Track error rates by code:
// Metrics dashboard
{
"error_rates": {
"VALIDATION_ERROR": 0.05, // 5% of requests
"UNAUTHORIZED": 0.02, // 2% of requests
"RATE_LIMITED": 0.01, // 1% of requests
"SERVER_ERROR": 0.0001 // 0.01% of requests (🚨 alert if > 0.01%)
}
}Alert Conditions
Critical alerts:
SERVER_ERRORrate > 0.1%SERVICE_UNAVAILABLE> 0- Database connection failures
Warning alerts:
RATE_LIMITEDspike (may indicate DDoS or integration bug)UNAUTHORIZEDspike (credential leakage?)VALIDATION_ERRORspike on new form (bad client-side validation)
Error Budgets
Set error budgets per service:
error_budget:
target_success_rate: 99.9%
measurement_window: 30d
budget_remaining: 97.2% # Still have 97.2% of error budget leftWhen budget exhausted:
- Freeze feature releases
- Focus on reliability improvements
- Investigate root causes
Security Considerations
❌ Never Leak Sensitive Info
Bad:
{
"error": {
"code": "UNAUTHORIZED",
"message": "Password incorrect for user john@acme.com",
"details": {
"attempted_password": "Password123!", // 🚨 NEVER DO THIS
"actual_password_hash": "bcrypt$..." // 🚨 NEVER DO THIS
}
}
}Good:
{
"error": {
"code": "UNAUTHORIZED",
"message": "Invalid credentials",
"details": null
}
}❌ Don't Confirm Resource Existence
Bad:
// Attacker probes: DELETE /api/v1/data/account/acc_123
{
"error": {
"code": "FORBIDDEN",
"message": "You don't have permission to delete this account"
}
}
// Attacker learns: Account acc_123 exists! 🚨Good:
// Return NOT_FOUND for both "doesn't exist" and "exists but no permission"
{
"error": {
"code": "NOT_FOUND",
"message": "Account not found"
}
}✅ Rate Limit Error Responses
Even error responses can be abused:
// Attacker tries to enumerate user emails
for (let i = 0; i < 1000000; i++) {
await register({ email: `user${i}@example.com` });
// Response: "ALREADY_EXISTS" or "VALIDATION_ERROR"
}Solution: Rate limit failed registration attempts:
{
"error": {
"code": "RATE_LIMITED",
"message": "Too many failed registration attempts"
}
}