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
 
