Error Handling
This guide explains how to handle errors when making HTTP requests with the Fetch HTTP package.
Response Status Checking
The most common way to handle HTTP errors is by checking the response status:
// Make a request
$response = get('https://api.example.com/users/123');
if ($response->successful()) {
// Status code is 2xx - process the response
$user = $response->json();
echo "Found user: {$user['name']}";
} else {
// Error occurred - handle based on status code
echo "Error: " . $response->status() . " " . $response->statusText();
}
Status Category Methods
The Response
class provides methods to check different status code categories:
$response = get('https://api.example.com/users/123');
if ($response->successful()) {
// Status code is 2xx
$user = $response->json();
echo "Found user: {$user['name']}";
} elseif ($response->isClientError()) {
// Status code is 4xx
echo "Client error: " . $response->status();
} elseif ($response->isServerError()) {
// Status code is 5xx
echo "Server error: " . $response->status();
} elseif ($response->isRedirection()) {
// Status code is 3xx
echo "Redirect to: " . $response->header('Location');
}
Specific Status Code Methods
For handling specific status codes, the Response
class provides dedicated methods:
if ($response->isOk()) {
// 200 OK
$data = $response->json();
} elseif ($response->isNotFound()) {
// 404 Not Found
echo "Resource not found";
} elseif ($response->isUnauthorized()) {
// 401 Unauthorized
echo "Authentication required";
} elseif ($response->isForbidden()) {
// 403 Forbidden
echo "Access denied";
} elseif ($response->isUnprocessableEntity()) {
// 422 Unprocessable Entity
$errors = $response->json()['errors'] ?? [];
foreach ($errors as $field => $messages) {
echo "{$field}: " . implode(', ', $messages) . "\n";
}
}
Using Status Enums
Fetch PHP provides type-safe enums for status codes, which you can use for more explicit comparisons:
use Fetch\Enum\Status;
// Get the status as an enum
$statusEnum = $response->statusEnum();
// Compare with enum values
if ($statusEnum === Status::OK) {
// Status is exactly 200 OK
} elseif ($statusEnum === Status::NOT_FOUND) {
// Status is exactly 404 Not Found
} elseif ($statusEnum === Status::TOO_MANY_REQUESTS) {
// Status is exactly 429 Too Many Requests
}
// Check using isStatus() with enum
if ($response->isStatus(Status::CREATED)) {
// Status is 201 Created
}
Exception Handling
When network errors or other exceptions occur, they are thrown as PHP exceptions:
use Fetch\Exceptions\NetworkException;
use Fetch\Exceptions\RequestException;
use Fetch\Exceptions\ClientException;
use Fetch\Exceptions\TimeoutException;
try {
$response = get('https://api.example.com/users');
if ($response->failed()) {
throw new \Exception("Request failed with status: " . $response->status());
}
$users = $response->json();
} catch (NetworkException $e) {
// Network-related issues (DNS failure, connection refused, etc.)
echo "Network error: " . $e->getMessage();
} catch (TimeoutException $e) {
// Request timed out
echo "Request timed out: " . $e->getMessage();
} catch (RequestException $e) {
// HTTP request errors
echo "Request error: " . $e->getMessage();
// If the exception has a response, you can still access it
if ($e->hasResponse()) {
$errorResponse = $e->getResponse();
$statusCode = $errorResponse->status();
$errorDetails = $errorResponse->json()['error'] ?? 'Unknown error';
echo "Status: {$statusCode}, Error: {$errorDetails}";
}
} catch (ClientException $e) {
// General client errors
echo "Client error: " . $e->getMessage();
} catch (\Exception $e) {
// Catch any other exceptions
echo "Error: " . $e->getMessage();
}
Handling JSON Decoding Errors
When decoding JSON responses, you may encounter parsing errors:
try {
$data = $response->json();
} catch (\RuntimeException $e) {
// JSON parsing failed
echo "Failed to decode JSON: " . $e->getMessage();
// You can access the raw response body
$rawBody = $response->body();
echo "Raw response: " . $rawBody;
}
To suppress JSON decoding errors:
// Pass false to disable throwing exceptions
$data = $response->json(true, false);
// Or use the array method with error suppression
$data = $response->array(false);
// Or use the get method with a default
$value = $response->get('key', 'default value');
Handling Validation Errors
Many APIs return validation errors with status 422 (Unprocessable Entity):
$response = post('https://api.example.com/users', [
'email' => 'invalid-email',
'password' => '123' // Too short
]);
if ($response->isUnprocessableEntity()) {
$errors = $response->json()['errors'] ?? [];
foreach ($errors as $field => $messages) {
echo "- {$field}: " . implode(', ', $messages) . "\n";
}
}
Common API Error Formats
Different APIs structure their error responses differently. Here's how to handle some common formats:
Standard JSON API Errors
if ($response->failed()) {
$errorData = $response->json(true, false); // Don't throw on parse errors
// Format: { "error": { "code": "invalid_token", "message": "The token is invalid" } }
if (isset($errorData['error']['message'])) {
echo "Error: " . $errorData['error']['message'];
echo "Code: " . $errorData['error']['code'] ?? 'unknown';
}
// Format: { "errors": [{ "title": "Invalid token", "detail": "The token is expired" }] }
elseif (isset($errorData['errors']) && is_array($errorData['errors'])) {
foreach ($errorData['errors'] as $error) {
echo $error['title'] . ": " . ($error['detail'] ?? '') . "\n";
}
}
// Format: { "message": "Validation failed", "errors": { "email": ["Invalid email"] } }
elseif (isset($errorData['message']) && isset($errorData['errors'])) {
echo $errorData['message'] . "\n";
foreach ($errorData['errors'] as $field => $messages) {
echo "- {$field}: " . implode(', ', $messages) . "\n";
}
}
// Simple format: { "message": "An error occurred" }
elseif (isset($errorData['message'])) {
echo "Error: " . $errorData['message'];
}
// Fallback
else {
echo "Unknown error occurred. Status code: " . $response->status();
}
}
Retry on Error
You can automatically retry requests that fail due to transient errors:
use Fetch\Http\ClientHandler;
$response = ClientHandler::create()
// Retry up to 3 times with exponential backoff
->retry(3, 100)
// Customize which status codes to retry
->retryStatusCodes([429, 503, 504])
// Customize which exceptions to retry
->retryExceptions([\GuzzleHttp\Exception\ConnectException::class])
->get('https://api.example.com/unstable-endpoint');
Handling Rate Limits
Many APIs implement rate limiting. Here's how to handle 429 Too Many Requests responses:
$response = get('https://api.example.com/users');
if ($response->isTooManyRequests()) {
// Check for Retry-After header (might be in seconds or a timestamp)
$retryAfter = $response->header('Retry-After');
if ($retryAfter !== null) {
if (is_numeric($retryAfter)) {
$waitSeconds = (int) $retryAfter;
} else {
// Parse HTTP date
$waitSeconds = strtotime($retryAfter) - time();
}
echo "Rate limited. Please try again after {$waitSeconds} seconds.";
// You could wait and retry automatically
if ($waitSeconds > 0 && $waitSeconds < 60) { // Only wait if reasonable
sleep($waitSeconds);
return get('https://api.example.com/users');
}
} else {
echo "Rate limited. Please try again later.";
}
}
Asynchronous Error Handling
When working with asynchronous requests, you can use try/catch blocks with await or the catch method with promises:
use function async;
use function await;
// Using try/catch with await
await(async(function() {
try {
$response = await(async(function() {
return fetch('https://api.example.com/users/999');
}));
if ($response->failed()) {
throw new \Exception("Request failed with status: " . $response->status());
}
return $response->json();
} catch (\Exception $e) {
echo "Error: " . $e->getMessage();
return [];
}
}));
// Using catch() with promises
$handler = fetch_client()->getHandler();
$handler->async()
->get('https://api.example.com/users/999')
->then(function($response) {
if ($response->failed()) {
throw new \Exception("API returned error: " . $response->status());
}
return $response->json();
})
->catch(function($error) {
echo "Error: " . $error->getMessage();
return [];
});
Custom Error Handling Class
For more advanced applications, you might want to create a dedicated error handler:
class ApiErrorHandler
{
/**
* Handle a response that might contain errors.
*/
public function handleResponse($response)
{
if ($response->successful()) {
return $response;
}
switch ($response->status()) {
case 401:
throw new AuthenticationException("Authentication required");
case 403:
throw new AuthorizationException("You don't have permission to access this resource");
case 404:
throw new ResourceNotFoundException("The requested resource was not found");
case 422:
$errors = $response->json()['errors'] ?? [];
throw new ValidationException("Validation failed", $errors);
case 429:
$retryAfter = $response->header('Retry-After');
throw new RateLimitException("Too many requests", $retryAfter);
case 500:
case 502:
case 503:
case 504:
throw new ServerException("Server error: " . $response->status());
default:
throw new ApiException("API error: " . $response->status());
}
}
}
// Usage
$errorHandler = new ApiErrorHandler();
try {
$response = get('https://api.example.com/users');
$errorHandler->handleResponse($response);
// Process successful response
$users = $response->json();
} catch (AuthenticationException $e) {
// Handle authentication error
} catch (ValidationException $e) {
// Handle validation errors
$errors = $e->getErrors();
} catch (ApiException $e) {
// Handle other API errors
}
Debugging Errors
For debugging, you can get detailed information about a request:
$handler = fetch_client()->getHandler();
$debugInfo = $handler->debug();
try {
// Attempt the request
$response = $handler->get('https://api.example.com/users');
if ($response->failed()) {
echo "Request failed with status: " . $response->status() . "\n";
echo "Debug information:\n";
print_r($debugInfo);
}
} catch (\Exception $e) {
echo "Exception: " . $e->getMessage() . "\n";
echo "Debug information:\n";
print_r($debugInfo);
}
Error Logging
You can use a PSR-3 compatible logger to log errors:
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
// Create a logger
$logger = new Logger('api');
$logger->pushHandler(new StreamHandler('logs/api.log', Logger::ERROR));
// Set the logger on the client
$client = fetch_client();
$client->setLogger($logger);
// Now errors will be logged
try {
$response = $client->get('https://api.example.com/users');
if ($response->failed()) {
// This will be logged by the client
throw new \Exception("API request failed: " . $response->status());
}
} catch (\Exception $e) {
// Additional custom logging if needed
$logger->error("Custom error handler: " . $e->getMessage());
}
Error Handling with Retries and Logging
Combining retries, logging, and error handling for robust API interactions:
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
// Create a logger
$logger = new Logger('api');
$logger->pushHandler(new StreamHandler('logs/api.log', Logger::INFO));
// Configure client with retry logic and logging
$client = fetch_client()
->getHandler()
->setLogger($logger)
->retry(3, 500) // 3 retries with 500ms initial delay
->retryStatusCodes([429, 500, 502, 503, 504]);
try {
$response = $client->get('https://api.example.com/flaky-endpoint');
if ($response->failed()) {
if ($response->isUnauthorized()) {
// Handle authentication issues
throw new \Exception("Authentication required");
} elseif ($response->isForbidden()) {
// Handle permission issues
throw new \Exception("Permission denied");
} else {
// Handle other errors
throw new \Exception("API error: " . $response->status());
}
}
// Process successful response
$data = $response->json();
} catch (\Exception $e) {
// Handle the exception after retries are exhausted
$logger->error("Failed after retries: " . $e->getMessage());
// Provide user-friendly message
echo "We're having trouble connecting to the service. Please try again later.";
}
Next Steps
- Learn about Retry Handling for automatic recovery from errors
- Explore Logging for more advanced error logging
- See Authentication for handling authentication errors
- Check out Asynchronous Requests for handling errors in async operations