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.

Leave a Reply