API Error Handling
Implement comprehensive, standardized error response system for PHP REST APIs with consistent JSON envelopes, specific error message extraction, and SweetAlert2 integration.
Core Principles:
- •Consistent JSON envelope across all endpoints
- •Specific error messages extracted from all exception types
- •Appropriate HTTP status codes for all error categories
- •Machine-readable error codes for programmatic handling
- •Human-readable messages for SweetAlert2 display
- •Secure error handling (no stack traces in production)
- •Comprehensive logging with request IDs
See subdirectories for:
- •
references/- Complete PHP classes (ApiResponse, ExceptionHandler, Exceptions) - •
examples/- Full endpoint implementation, frontend client
Response Envelope Standard
Success:
{
"success": true,
"data": { /* payload */ },
"message": "Optional success message",
"meta": {
"timestamp": "2026-01-24T10:30:00Z",
"request_id": "req_abc123"
}
}
Error:
{
"success": false,
"message": "Human-readable error for SweetAlert2",
"error": {
"code": "ERROR_CODE",
"type": "validation_error",
"details": { /* field-specific errors */ }
},
"meta": {
"timestamp": "2026-01-24T10:30:00Z",
"request_id": "req_abc123"
}
}
HTTP Status Codes
| Status | Error Type | Use Case | Error Code Examples |
|---|---|---|---|
| 400 | Bad Request | Malformed JSON, missing params | INVALID_JSON, MISSING_PARAMETER |
| 401 | Unauthorized | Missing/invalid auth token | TOKEN_MISSING, TOKEN_EXPIRED |
| 403 | Forbidden | Valid auth but no permission | PERMISSION_DENIED, ACCESS_FORBIDDEN |
| 404 | Not Found | Resource doesn't exist | RESOURCE_NOT_FOUND, INVOICE_NOT_FOUND |
| 405 | Method Not Allowed | Wrong HTTP method | METHOD_NOT_ALLOWED |
| 409 | Conflict | Business rule violation | ALREADY_EXISTS, OVERPAYMENT |
| 422 | Unprocessable Entity | Validation errors | VALIDATION_FAILED, INVALID_EMAIL |
| 429 | Too Many Requests | Rate limiting | RATE_LIMIT_EXCEEDED |
| 500 | Internal Server Error | Unexpected errors | INTERNAL_ERROR, DATABASE_ERROR |
| 503 | Service Unavailable | Maintenance/overload | SERVICE_UNAVAILABLE, DEADLOCK |
ApiResponse Helper (Quick Reference)
See references/ApiResponse.php for complete implementation
// Success responses ApiResponse::success($data, $message, $status); ApiResponse::created($data, $message); // Error responses ApiResponse::error($message, $code, $status, $type, $details); ApiResponse::validationError($errors, $message); ApiResponse::notFound($resource, $identifier); ApiResponse::unauthorized($message); ApiResponse::forbidden($permission); ApiResponse::conflict($message, $code); ApiResponse::methodNotAllowed($allowedMethods); ApiResponse::rateLimited($retryAfter); ApiResponse::serverError($message); ApiResponse::serviceUnavailable($message);
Exception Handler (Quick Reference)
See references/ExceptionHandler.php for complete implementation
Key Features:
- •Extracts specific messages from PDOException
- •Parses SQLSTATE 45000 (user-defined exceptions from triggers)
- •Extracts constraint violation messages
- •Handles deadlocks gracefully
- •Logs all errors with request ID
- •Hides stack traces in production
PDOException Parsing:
// SQLSTATE 45000: Business rule from trigger // "SQLSTATE[45000]: <<1>>: 1644 Overpayment not allowed" // Extracted: "Overpayment not allowed" // SQLSTATE 23000: Duplicate entry // "Duplicate entry 'john@example.com' for key 'uk_email'" // Extracted: "A record with this Email already exists: 'john@example.com'" // SQLSTATE 23000: Foreign key violation // "Cannot delete or update a parent row..." // Extracted: "Referenced Customer does not exist or cannot be deleted"
Custom Exception Classes
See references/CustomExceptions.php for all classes
// Validation (422)
throw new ValidationException([
'email' => 'Invalid email format',
'phone' => 'Phone number required'
], 'Validation failed');
// Authentication (401)
throw new AuthenticationException('Token expired', 'expired');
// Authorization (403)
throw new AuthorizationException('MANAGE_USERS');
// Not Found (404)
throw new NotFoundException('Invoice', 'INV-123');
// Conflict (409)
throw new ConflictException('Already voided', 'ALREADY_VOIDED');
// Rate Limit (429)
throw new RateLimitException(60);
API Bootstrap Pattern
See references/bootstrap.php for complete file
Include at top of all API endpoints:
require_once __DIR__ . '/bootstrap.php';
// Helper functions available:
require_method(['POST', 'PUT']);
$data = read_json_body();
validate_required($data, ['customer_id', 'amount']);
$db = get_db();
$token = bearer_token();
require_auth();
require_permission('MANAGE_INVOICES');
handle_request(function() { /* endpoint logic */ });
Endpoint Implementation Pattern
See examples/InvoicesEndpoint.php for complete example
require_once __DIR__ . '/../bootstrap.php';
use App\Http\ApiResponse;
use App\Http\Exceptions\{NotFoundException, ValidationException};
require_auth();
handle_request(function() {
$method = $_SERVER['REQUEST_METHOD'];
match ($method) {
'POST' => handlePost(),
default => ApiResponse::methodNotAllowed('POST')
};
});
function handlePost(): void {
$data = read_json_body();
validate_required($data, ['customer_id', 'items']);
if (empty($data['items'])) {
throw new ValidationException(['items' => 'Required']);
}
// Business logic...
ApiResponse::created(['id' => $id], 'Created successfully');
}
Frontend Integration
See examples/ApiClient.js for complete implementation
const api = new ApiClient('./api');
// GET - Errors automatically shown via SweetAlert2
const response = await api.get('invoices.php', { status: 'pending' });
if (response) renderInvoices(response.data);
// POST - Validation errors highlight form fields
showLoading('Creating...');
const result = await api.post('invoices.php', formData);
hideLoading();
if (result) showSuccess('Created successfully');
// DELETE - With confirmation
const { value: reason } = await Swal.fire({
title: 'Void?',
input: 'textarea',
showCancelButton: true
});
if (reason) {
const res = await api.delete(`invoices.php?id=${id}`, { reason });
if (res) showSuccess('Voided');
}
// Helpers: showSuccess/Error/Warning/Info, showConfirm, showLoading, hideLoading
Error Message Extraction
SQLSTATE 45000 (Trigger):
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Overpayment not allowed'; -- Extracted: "Overpayment not allowed" → Code: OVERPAYMENT_NOT_ALLOWED
SQLSTATE 23000 (Duplicate):
"Duplicate entry 'john@example.com' for key 'uk_email'" -- Extracted: "A record with this Email already exists: 'john@example.com'"
SQLSTATE 23000 (Foreign Key):
"Cannot delete or update a parent row..." -- Extracted: "Referenced Customer does not exist or cannot be deleted"
Deadlock:
"Deadlock found when trying to get lock..." -- Extracted: "Database conflict. Please try again." → HTTP 503
Validation Pattern
Backend:
$errors = [];
if (empty($data['email'])) $errors['email'] = 'Email required';
elseif (!filter_var($data['email'], FILTER_VALIDATE_EMAIL))
$errors['email'] = 'Invalid email';
if ($errors) ApiResponse::validationError($errors);
Frontend automatically:
- •SweetAlert2 with formatted error list
- •Highlights fields with
.is-invalid - •Adds
.invalid-feedbackelements
Business Rule Pattern
$stmt = $db->prepare("SELECT status FROM invoices WHERE id = ? FOR UPDATE");
$stmt->execute([$id]);
$invoice = $stmt->fetch();
if (!$invoice) throw new NotFoundException('Invoice', $id);
if ($invoice['status'] === 'voided')
throw new ConflictException('Already voided', 'ALREADY_VOIDED');
if ($invoice['status'] === 'paid')
throw new ConflictException('Cannot void paid invoice', 'INVOICE_PAID');
Logging Pattern
All errors logged with context:
[req_abc123] PDOException: SQLSTATE[45000]: Overpayment not allowed in /api/payments.php:45
Context: {"user_id":123,"franchise_id":5,"url":"/api/payments.php","method":"POST"}
Implementation Checklist
Backend:
- • All endpoints use
ApiResponsehelper - • All exceptions caught by
ExceptionHandler - • Specific error codes for each error type
- • PDOException messages properly extracted
- • Trigger errors parsed (SQLSTATE 45000)
- • Constraint violations give meaningful messages
- • Stack traces hidden in production
- • All errors logged with request ID
- • HTTP status codes match error types
- • Bootstrap file included in all endpoints
Frontend:
- • All API calls use centralized
ApiClient - • Errors displayed via SweetAlert2 only (no native alert/confirm)
- • Validation errors highlight form fields
- • Network errors handled gracefully
- • Loading states shown during requests
- • Error codes displayed in footer
- • Offline state detected and handled
- • Request IDs logged for debugging
Database:
- • Triggers use SQLSTATE 45000 for business rules
- • Clear, user-friendly error messages in triggers
- • Constraints have descriptive names (uk_email_franchise)
- • Foreign key errors give context
Quick Error Reference
// 400 - Bad Request
ApiResponse::error('Invalid request', 'BAD_REQUEST', 400);
// 401 - Unauthorized
ApiResponse::unauthorized('Session expired');
// 403 - Forbidden
ApiResponse::forbidden('MANAGE_USERS');
// 404 - Not Found
ApiResponse::notFound('Invoice', 'INV-123');
// 409 - Conflict
ApiResponse::conflict('Already voided', 'ALREADY_VOIDED');
// 422 - Validation
ApiResponse::validationError(['email' => 'Invalid format']);
// 500 - Server Error
ApiResponse::serverError('Unexpected error occurred');
Security Considerations
Production:
- •Never expose stack traces
- •Sanitize database error messages
- •Log full errors server-side only
- •Generic messages for unexpected errors
Development:
- •Set
APP_DEBUG=truefor detailed errors - •Stack traces in logs
- •Actual error messages displayed
Sensitive Data:
- •Never include passwords in logs
- •Sanitize SQL queries in error messages
- •Remove file paths from production errors
Summary
Implementation:
- •Include
bootstrap.phpat top of all endpoints - •Use
ApiResponsehelper for all responses - •Throw custom exceptions for specific errors
- •Let
ExceptionHandlerconvert to JSON - •Use
ApiClientclass on frontend - •Display all errors via SweetAlert2
Key Files:
- •
references/ApiResponse.php- Response helper - •
references/ExceptionHandler.php- Exception converter - •
references/CustomExceptions.php- Exception classes - •
references/bootstrap.php- API setup - •
examples/InvoicesEndpoint.php- Complete endpoint - •
examples/ApiClient.js- Frontend client
Benefits:
- •Consistent error format across all endpoints
- •Specific messages from database exceptions
- •Beautiful error display with SweetAlert2
- •Easy debugging with request IDs
- •Secure (no sensitive data exposure)
- •Developer-friendly (clear patterns)
Remember: All error messages must be suitable for direct display in SweetAlert2. Write them for end users, not developers.