Skip to content

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:

php
use function Fetch\Http\get;

$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:

php
$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->clientError()) {
    // Status code is 4xx
    echo "Client error: " . $response->status();
} elseif ($response->serverError()) {
    // Status code is 5xx
    echo "Server error: " . $response->status();
} elseif ($response->redirect()) {
    // 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:

php
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";
    }
}

Exception Handling

When network errors or other exceptions occur, they are thrown as PHP exceptions:

php
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:

php
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:

php
// Pass false to disable throwing exceptions
$data = $response->json(true, 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):

php
$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

php
if ($response->failed()) {
    $errorData = $response->json();

    // 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:

php
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:

php
$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.";
    }
}

Custom Error Handling Class

For more advanced applications, you might want to create a dedicated error handler:

php
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 use the debug() method to get detailed information about a request:

php
try {
    $client = fetch();
    $debug = $client->debug();

    // Attempt the request
    $response = $client->get('https://api.example.com/users');

    if ($response->failed()) {
        echo "Request failed with status: " . $response->status() . "\n";
        echo "Debug information:\n";
        print_r($debug);
    }
} catch (\Exception $e) {
    echo "Exception: " . $e->getMessage() . "\n";
    echo "Debug information:\n";
    print_r($debug);
}

Error Logging

You can use a PSR-3 compatible logger to log errors:

php
use Monolog\Logger;
use Monolog\Handler\StreamHandler;

// Create a logger
$logger = new Logger('api');
$logger->pushHandler(new StreamHandler('logs/api.log', Logger::ERROR));

// Configure the client with the logger
fetch_client(logger: $logger);

// Now errors will be logged
try {
    $response = 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());
}

Next Steps

Created by Jerome Thayananthajothy and released under the MIT License.