Error Handling & Status Codes
Status: Final
Version: 1.0
Last Updated: 2026-01-03
Purpose
Define the standard error response model, error codes, HTTP status codes, and error handling patterns for all ESG platform APIs.
Standard Error Response
Schema:
{
"error": {
"code": "ERROR_CODE",
"message": "Human-readable error message",
"details": {}, // Optional: field-level errors or context
"request_id": "req_abc123",
"timestamp": "2026-01-03T14:30:00Z"
}
}
Example - Validation Error:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "The given data was invalid.",
"details": {
"value": ["The value must be a number.", "The value must be at least 0."],
"evidence_files": ["At least one evidence file is required."]
},
"request_id": "req_550e8400",
"timestamp": "2026-01-03T14:30:00Z"
}
}
HTTP Status Codes
| Status |
Code |
Usage |
| Success |
|
|
| OK |
200 |
Successful GET, PUT, PATCH |
| Created |
201 |
Successful POST (resource created) |
| Accepted |
202 |
Async job queued |
| No Content |
204 |
Successful DELETE |
| Client Errors |
|
|
| Bad Request |
400 |
Malformed JSON, invalid parameters |
| Unauthorized |
401 |
Missing/invalid/expired token |
| Forbidden |
403 |
Valid token but insufficient permissions |
| Not Found |
404 |
Resource doesn't exist |
| Conflict |
409 |
Duplicate submission, state conflict |
| Unprocessable Entity |
422 |
Validation failed |
| Too Many Requests |
429 |
Rate limit exceeded |
| Server Errors |
|
|
| Internal Server Error |
500 |
Unexpected error |
| Service Unavailable |
503 |
Maintenance mode, queue full |
Error Codes
Authentication & Authorization (AUTH_*)
| Code |
HTTP Status |
Description |
Action |
AUTH_TOKEN_MISSING |
401 |
No Authorization header |
Provide JWT token |
AUTH_TOKEN_INVALID |
401 |
Malformed or tampered token |
Re-authenticate |
AUTH_TOKEN_EXPIRED |
401 |
Token past expiry |
Use refresh token |
AUTH_INSUFFICIENT_PERMISSIONS |
403 |
User role lacks permission |
Contact admin for access |
AUTH_TENANT_MISMATCH |
403 |
Token tenant ≠ X-Tenant-Id header |
Fix tenant context |
AUTH_SCOPE_VIOLATION |
403 |
User lacks site/project access |
Request access from admin |
Validation (VALIDATION_*)
| Code |
HTTP Status |
Description |
Details Field |
VALIDATION_ERROR |
422 |
Field validation failed |
Field-level errors |
VALIDATION_RULE_FAILED |
422 |
Business rule violated |
Rule name + details |
VALIDATION_EVIDENCE_MISSING |
422 |
Required evidence not attached |
Required evidence types |
VALIDATION_ANOMALY_DETECTED |
200 (warning) |
Outlier detected |
Warning message |
Resource Errors (RESOURCE_*)
| Code |
HTTP Status |
Description |
RESOURCE_NOT_FOUND |
404 |
Entity doesn't exist or user lacks access |
RESOURCE_ALREADY_EXISTS |
409 |
Duplicate resource (e.g., submission with same UUID) |
RESOURCE_LOCKED |
409 |
Reporting period locked, no edits allowed |
RESOURCE_DELETED |
410 |
Resource was soft-deleted |
State Errors (STATE_*)
| Code |
HTTP Status |
Description |
Example |
STATE_TRANSITION_INVALID |
409 |
Illegal state transition |
Can't approve a rejected submission |
STATE_PREREQUISITE_MISSING |
409 |
Prerequisite not met |
Can't lock period with unreviewed submissions |
Rate Limiting (RATE_*)
| Code |
HTTP Status |
Description |
Headers |
RATE_LIMIT_EXCEEDED |
429 |
Too many requests |
X-RateLimit-Reset, Retry-After |
System Errors (SYSTEM_*)
| Code |
HTTP Status |
Description |
SYSTEM_ERROR |
500 |
Unexpected internal error |
SYSTEM_MAINTENANCE |
503 |
Scheduled maintenance |
SYSTEM_DATABASE_ERROR |
503 |
Database unavailable |
SYSTEM_QUEUE_FULL |
503 |
Background queue at capacity |
Laravel Implementation
Exception Handler
// app/Exceptions/Handler.php
class Handler extends ExceptionHandler
{
public function render($request, Throwable $exception)
{
if ($request->expectsJson()) {
return $this->handleApiException($request, $exception);
}
return parent::render($request, $exception);
}
protected function handleApiException($request, $exception)
{
$status = 500;
$code = 'SYSTEM_ERROR';
$message = 'An unexpected error occurred.';
$details = null;
if ($exception instanceof ValidationException) {
$status = 422;
$code = 'VALIDATION_ERROR';
$message = 'The given data was invalid.';
$details = $exception->errors();
} elseif ($exception instanceof AuthenticationException) {
$status = 401;
$code = 'AUTH_TOKEN_INVALID';
$message = $exception->getMessage();
} elseif ($exception instanceof AuthorizationException) {
$status = 403;
$code = 'AUTH_INSUFFICIENT_PERMISSIONS';
$message = $exception->getMessage();
} elseif ($exception instanceof ModelNotFoundException) {
$status = 404;
$code = 'RESOURCE_NOT_FOUND';
$message = 'The requested resource was not found.';
} elseif ($exception instanceof HttpException) {
$status = $exception->getStatusCode();
$code = $this->getErrorCodeForStatus($status);
$message = $exception->getMessage() ?: Response::$statusTexts[$status];
}
return response()->json([
'error' => [
'code' => $code,
'message' => $message,
'details' => $details,
'request_id' => $request->attributes->get('request_id'),
'timestamp' => now()->toIso8601String(),
]
], $status);
}
}
Custom Exceptions
// app/Exceptions/StateTransitionException.php
class StateTransitionException extends Exception
{
public function __construct($from, $to, $reason)
{
$message = "Cannot transition from {$from} to {$to}: {$reason}";
parent::__construct($message);
}
public function render($request)
{
return response()->json([
'error' => [
'code' => 'STATE_TRANSITION_INVALID',
'message' => $this->getMessage(),
'request_id' => $request->attributes->get('request_id'),
]
], 409);
}
}
Acceptance Criteria
Done When:
- [ ] All API endpoints return standard error JSON
- [ ] Field-level validation errors included in details
- [ ] Request IDs logged and returned in errors
- [ ] HTTP status codes match error scenarios
- [ ] Rate limit errors include Retry-After header
Cross-References
Change Log
| Version |
Date |
Author |
Changes |
| 1.0 |
2026-01-03 |
Senior Product Architect |
Initial error handling specification |