Catch ConnectException Before RequestException in Guzzle

📖 2 minutes read

When you catch Guzzle exceptions, you’re probably writing something like this:

use GuzzleHttp\Exception\RequestException;

try {
    $response = $client->request('POST', '/api/orders');
} catch (RequestException $e) {
    // Handle error
    Log::error('API call failed: ' . $e->getMessage());
}

This works, but it treats every failure the same way. A connection timeout is fundamentally different from a 400 Bad Request — and your error handling should reflect that.

Guzzle has a clear exception hierarchy:

  • TransferException (base class)
    • RequestException (server returned a response, but it was an error)
      • ConnectException (couldn’t connect at all — timeouts, DNS failures, refused connections)

The key insight: ConnectException extends RequestException, so if you catch RequestException first, you’ll catch both. But if you catch ConnectException first, you can handle connection issues differently:

use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Exception\RequestException;

try {
    $response = $client->request('POST', '/api/orders', [
        'json' => $payload,
    ]);
} catch (ConnectException $e) {
    // The server is unreachable — retry later, don't log the payload
    throw new ServiceTemporarilyUnavailableException(
        'Could not reach external API: ' . $e->getMessage(),
        $e->getCode(),
        $e
    );
} catch (RequestException $e) {
    // We got a response, but it was an error (4xx, 5xx)
    $statusCode = $e->getResponse()?->getStatusCode();
    Log::error("API returned {$statusCode}", [
        'body' => $e->getResponse()?->getBody()->getContents(),
    ]);
    throw $e;
}

Why does this matter? Because the recovery strategy is different:

  • Connection failures are transient — retry with backoff, mark the service as temporarily unavailable, or delete the job from the queue so it doesn’t burn through retry attempts
  • HTTP errors (4xx/5xx) might be permanent — a 422 means your payload is wrong, retrying won’t help

In queue jobs, this distinction is especially powerful. You can catch ConnectException and throw a domain-specific exception that your job’s failed() method handles differently — maybe releasing the job back to the queue with a delay instead of marking it as permanently failed.

Next time you’re writing a try/catch around an HTTP call, ask yourself: “Am I handling connection failures differently from response errors?” If not, add that ConnectException catch block. Your future debugging self will thank you.

Daryle De Silva

VP of Technology

11+ years building and scaling web applications. Writing about what I learn in the trenches.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *