friendship ended with social-app. php is my new best friend

title: Error Handling description: Learn how to handle errors and exceptions with the Fetch HTTP package#

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#