Error Handling Examples
This page demonstrates robust error handling techniques when working with HTTP requests using the Fetch HTTP package.
Basic Error Handling
Checking for success and handling errors:
php
use function Fetch\Http\fetch;
$response = fetch('https://api.example.com/users');
if ($response->successful()) {
// Status code is 2xx
$users = $response->json();
// Process the users...
} elseif ($response->clientError()) {
// Status code is 4xx
echo "Client error: " . $response->status();
} elseif ($response->serverError()) {
// Status code is 5xx
echo "Server error: " . $response->status();
} else {
// Other status code
echo "Unexpected status: " . $response->status();
}
Using Specific Status Methods
The Response class provides dedicated methods for checking specific status codes:
php
use function Fetch\Http\fetch;
$response = fetch('https://api.example.com/users/123');
if ($response->isOk()) {
// 200 OK
$user = $response->json();
echo "Found user: " . $user['name'];
} elseif ($response->isNotFound()) {
// 404 Not Found
echo "User 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";
}
}
Try-Catch Exception Handling
Handling network and other exceptions:
php
use function Fetch\Http\fetch;
use Fetch\Exceptions\NetworkException;
use Fetch\Exceptions\RequestException;
use Fetch\Exceptions\TimeoutException;
try {
$response = fetch('https://api.example.com/users');
if ($response->failed()) {
throw new \Exception("Request failed with status: " . $response->status());
}
$users = $response->json();
// Process users...
} catch (NetworkException $e) {
// Network-related issues (DNS failure, connection refused, etc.)
echo "Network error: " . $e->getMessage();
logError('network', $e->getMessage());
} catch (TimeoutException $e) {
// Request timed out
echo "Request timed out: " . $e->getMessage();
logError('timeout', $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}";
}
logError('request', $e->getMessage());
} catch (\Exception $e) {
// Catch any other exceptions
echo "Error: " . $e->getMessage();
logError('general', $e->getMessage());
}
Handling Validation Errors
Working with validation errors from APIs:
php
use function Fetch\Http\post;
function createUser(array $userData)
{
$response = post('https://api.example.com/users', $userData);
if ($response->successful()) {
return $response->json();
}
if ($response->isUnprocessableEntity()) {
$errors = $response->json()['errors'] ?? [];
$formattedErrors = [];
foreach ($errors as $field => $messages) {
$formattedErrors[$field] = is_array($messages) ? $messages : [$messages];
}
throw new ValidationException(
"Validation failed: " . json_encode($formattedErrors),
$formattedErrors
);
}
throw new \RuntimeException(
"Failed to create user: " . $response->status() . " " . $response->body()
);
}
// Using the function
try {
$user = createUser([
'name' => 'John',
'email' => 'invalid-email'
]);
echo "User created with ID: " . $user['id'];
} catch (ValidationException $e) {
$errors = $e->getErrors();
echo "Please correct the following errors:\n";
foreach ($errors as $field => $messages) {
echo "- {$field}: " . implode(', ', $messages) . "\n";
}
} catch (\Exception $e) {
echo "Error: " . $e->getMessage();
}
// ValidationException definition
class ValidationException extends \Exception
{
private array $errors;
public function __construct(string $message, array $errors, int $code = 0)
{
parent::__construct($message, $code);
$this->errors = $errors;
}
public function getErrors(): array
{
return $this->errors;
}
}
Handling Rate Limits
Detecting and handling rate limiting:
php
use function Fetch\Http\fetch;
function fetchWithRateLimitHandling(string $url, int $maxRetries = 3)
{
$attempts = 0;
while ($attempts <= $maxRetries) {
$response = fetch($url);
if ($response->successful()) {
return $response;
}
if ($response->status() === 429) {
$attempts++;
// Check for Retry-After header
$retryAfter = $response->header('Retry-After');
if ($retryAfter !== null) {
// Retry-After can be in seconds or a HTTP date
if (is_numeric($retryAfter)) {
$delay = (int)$retryAfter;
} else {
$delay = max(1, strtotime($retryAfter) - time());
}
} else {
// Use exponential backoff if no Retry-After header
$delay = pow(2, $attempts);
}
if ($attempts <= $maxRetries) {
echo "Rate limited. Retrying in {$delay} seconds (attempt {$attempts}/{$maxRetries})...\n";
sleep($delay);
continue;
}
}
// If we get here, it's either not a rate limit or we've exceeded retries
break;
}
// If we've exhausted all retries or it's not a rate limit issue
if ($response->status() === 429) {
throw new \RuntimeException("Rate limit exceeded after {$maxRetries} retries");
}
throw new \RuntimeException(
"Request failed with status: " . $response->status() . " " . $response->body()
);
}
// Using the function
try {
$response = fetchWithRateLimitHandling('https://api.example.com/limited-endpoint');
$data = $response->json();
echo "Successfully fetched data after rate limiting";
} catch (\Exception $e) {
echo "Error: " . $e->getMessage();
}
Async Error Handling
Handling errors in asynchronous code:
php
use function Fetch\Http\fetch;
use function Matrix\async;
use function Matrix\await;
use function Matrix\all;
await(async(function() {
try {
$response = await(async(function() {
return fetch('https://api.example.com/users');
}));
if (!$response->successful()) {
throw new \RuntimeException(
"API error: " . $response->status() . " " . $response->body()
);
}
return $response->json();
} catch (\Exception $e) {
echo "Error: " . $e->getMessage();
return [];
}
}));
// Handling errors with promise combinators
try {
$results = await(all([
'users' => async(fn() => fetch('https://api.example.com/users')),
'posts' => async(fn() => fetch('https://api.example.com/posts'))
]));
// Check if any of the responses failed
foreach ($results as $key => $response) {
if (!$response->successful()) {
echo "{$key} request failed with status: " . $response->status() . "\n";
}
}
// Process successful responses
$users = $results['users']->successful() ? $results['users']->json() : [];
$posts = $results['posts']->successful() ? $results['posts']->json() : [];
} catch (\Exception $e) {
echo "Error in async operations: " . $e->getMessage();
}
Custom Error Handler Class
Creating a dedicated error handler:
php
use function Fetch\Http\fetch;
class ApiErrorHandler
{
/**
* Handle a response that might contain errors.
*/
public function handleResponse($response)
{
if ($response->successful()) {
return $response;
}
switch ($response->status()) {
case 400:
throw new BadRequestException(
$this->getErrorMessage($response, "Bad request")
);
case 401:
throw new AuthenticationException(
$this->getErrorMessage($response, "Authentication required")
);
case 403:
throw new AuthorizationException(
$this->getErrorMessage($response, "Permission denied")
);
case 404:
throw new ResourceNotFoundException(
$this->getErrorMessage($response, "Resource not found")
);
case 422:
$errors = $response->json()['errors'] ?? [];
throw new ValidationException(
$this->getErrorMessage($response, "Validation failed"),
$errors
);
case 429:
$retryAfter = $response->header('Retry-After');
throw new RateLimitException(
$this->getErrorMessage($response, "Too many requests"),
$retryAfter
);
case 500:
case 502:
case 503:
case 504:
throw new ServerException(
$this->getErrorMessage($response, "Server error: " . $response->status())
);
default:
throw new ApiException(
$this->getErrorMessage($response, "API error: " . $response->status())
);
}
}
/**
* Extract an error message from the response.
*/
private function getErrorMessage($response, string $default): string
{
$body = $response->json();
if (isset($body['message'])) {
return $body['message'];
}
if (isset($body['error'])) {
return is_string($body['error']) ? $body['error'] : json_encode($body['error']);
}
if (isset($body['errors']) && is_array($body['errors'])) {
if (isset($body['errors'][0])) {
// If errors is a simple array of messages
return implode(', ', $body['errors']);
} else {
// If errors is a field => messages structure
$messages = [];
foreach ($body['errors'] as $field => $fieldErrors) {
$messages[] = $field . ': ' . (is_array($fieldErrors) ? implode(', ', $fieldErrors) : $fieldErrors);
}
return implode('; ', $messages);
}
}
return $default;
}
}
// Exception classes
class ApiException extends \Exception {}
class BadRequestException extends ApiException {}
class AuthenticationException extends ApiException {}
class AuthorizationException extends ApiException {}
class ResourceNotFoundException extends ApiException {}
class ValidationException extends ApiException {
private array $errors;
public function __construct(string $message, array $errors, int $code = 0)
{
parent::__construct($message, $code);
$this->errors = $errors;
}
public function getErrors(): array
{
return $this->errors;
}
}
class RateLimitException extends ApiException {
private ?string $retryAfter;
public function __construct(string $message, ?string $retryAfter = null, int $code = 0)
{
parent::__construct($message, $code);
$this->retryAfter = $retryAfter;
}
public function getRetryAfter(): ?string
{
return $this->retryAfter;
}
}
class ServerException extends ApiException {}
// Usage
$errorHandler = new ApiErrorHandler();
try {
$response = fetch('https://api.example.com/users');
$errorHandler->handleResponse($response);
// Process successful response
$users = $response->json();
} catch (AuthenticationException $e) {
// Handle authentication error
echo "Please log in to continue: " . $e->getMessage();
} catch (ValidationException $e) {
// Handle validation errors
echo "Validation errors: " . $e->getMessage();
} catch (RateLimitException $e) {
// Handle rate limiting
$retryAfter = $e->getRetryAfter();
echo "Rate limited. Try again " . ($retryAfter ? "after {$retryAfter}" : "later");
} catch (ApiException $e) {
// Handle other API errors
echo "API error: " . $e->getMessage();
}
Error Reporting and Logging
Comprehensive error reporting:
php
use function Fetch\Http\fetch;
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
// Create a logger
$logger = new Logger('api');
$logger->pushHandler(new StreamHandler('logs/api.log', Logger::ERROR));
function fetchWithLogging(string $url, array $options = [])
{
global $logger;
try {
$startTime = microtime(true);
$response = fetch($url, $options);
$duration = microtime(true) - $startTime;
// Log all non-successful responses
if ($response->failed()) {
$logger->warning("API request failed", [
'url' => $url,
'method' => $options['method'] ?? 'GET',
'status' => $response->status(),
'duration' => round($duration, 3),
'response' => substr($response->body(), 0, 1000) // Limit response size in logs
]);
}
return $response;
} catch (\Exception $e) {
// Log exceptions
$logger->error("API request exception", [
'url' => $url,
'method' => $options['method'] ?? 'GET',
'exception' => get_class($e),
'message' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
throw $e;
}
}
// Using the function
try {
$response = fetchWithLogging('https://api.example