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

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

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

Using Status Enums

Fetch PHP provides type-safe enums for status codes, which you can use for more explicit comparisons:

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

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

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

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

Asynchronous Error Handling

When working with asynchronous requests, you can use try/catch blocks with await or the catch method with promises:

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

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 get detailed information about a request:

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

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

php
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

Released under the MIT License. A modern HTTP client for PHP developers.