In PHP's cURL extension, what does CURLOPT_RETURNTRANSFER change about curl_exec(), and how do you tell a failed transfer from an HTTP error?
answer
- print versus return the body
- curl_exec returns string|bool
- false means the transfer broke
- curl_errno and curl_error after false
- CURLINFO_RESPONSE_CODE for the status
basics
~20 sCURLOPT_RETURNTRANSFER makes curl_exec() return the body as a string instead of printing it. curl_exec() returns false only when the transfer itself fails (read curl_errno() and curl_error()); a 404 or 500 still succeeds, so check CURLINFO_RESPONSE_CODE.
solid answer
~40 sBy default `curl_exec()` writes the response body straight to output and returns `true`; with `CURLOPT_RETURNTRANSFER => true` it returns the body as a string. Either way it returns `false` only when the **transfer** fails: DNS, connection refused, a timeout, a TLS error. Then `curl_errno()` gives the `CURLE_*` code and `curl_error()` the message. An HTTP error status is not a transfer failure. A 404 or 503 comes back as a normal string, so after a successful exec I read `curl_getinfo($ch, CURLINFO_RESPONSE_CODE)` and decide myself. I compare with `=== false`, because an empty 204 body is `""`, which is falsy. Since PHP 8.0 `curl_init()` returns a `CurlHandle` object that is freed when it goes out of scope. `curl_close()` does nothing and is deprecated in PHP 8.5.
code
php · 22 lines<?php
declare(strict_types=1);
$ch = curl_init('https://rates.example.test/v1/quote?zip=10115');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Accept: application/json'],
]);
$body = curl_exec($ch);
if ($body === false) {
throw new RuntimeException(sprintf(
'Transfer failed (%d): %s', curl_errno($ch), curl_error($ch)
));
}
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
if ($status !== 200) {
throw new RuntimeException("Rates API answered HTTP $status");
}
$quote = json_decode($body, true, flags: JSON_THROW_ON_ERROR);go deeper
Recall the four calls and what RETURNTRANSFER does. Say clearly that curl_exec() returns false only on a transfer failure and that the status code comes from curl_getinfo().
Explain the two failure layers and where each is reported: curl_errno/curl_error for the transfer, CURLINFO_RESPONSE_CODE for the HTTP status. Mention why the check is === false, and what FAILONERROR trades away.
Show a checked wrapper that keeps the error body, logs curl_errno with timings from curl_getinfo, and never lets a 5xx page reach json_decode. Point out PHP 8.0's CurlHandle and the 8.5 curl_close deprecation during upgrades.
Frame the choice between raw cURL and a client library around who owns error mapping: a single shared wrapper that turns transfer and status failures into typed exceptions keeps every caller from reinventing the checks.
## The four calls every cURL request makes PHP's **cURL extension** wraps libcurl, the C transfer library. Every request follows the same lifecycle: 1. `curl_init(?string $url = null)` creates a handle. Since PHP 8.0 that handle is a `CurlHandle` object; the function returns `CurlHandle|false`. 2. `curl_setopt($ch, $option, $value)` or `curl_setopt_array($ch, $options)` configures it with `CURLOPT_*` constants. `curl_setopt_array()` stops at the first option that fails and returns `false`, ignoring the rest. 3. `curl_exec($ch)` performs the transfer. Its declared return type is `string|bool`. 4. `curl_getinfo($ch, ?int $option = null)` reads what happened: the status code, timings, the final URL. With no option it returns an array with keys such as `http_code`, `total_time` and `primary_ip`. ## What CURLOPT_RETURNTRANSFER changes Without the option, libcurl hands the body to PHP's default write callback, which **prints it**, just like `echo`. That is almost never what an application wants: the body ends up in the page or the CLI output and your variable holds only `true`. | Setting | `curl_exec()` on success | on transfer failure | |---|---|---| | `CURLOPT_RETURNTRANSFER` not set | prints the body, returns `true` | `false` | | `CURLOPT_RETURNTRANSFER => true` | returns the body as a `string` (possibly `""`) | `false` | The manual shows the difference in its return-value notes: `curl_exec()` returns the result when the option is set, and `true` otherwise. ## Two different kinds of failure This is the part interviewers probe. There are two layers, and cURL reports them in different places: - **Transfer failure**: the request never produced an HTTP response. Examples are a DNS failure, a refused connection, a timeout or a certificate that does not verify. `curl_exec()` returns `false`, `curl_errno($ch)` returns a non-zero `CURLE_*` code (`CURLE_OPERATION_TIMEDOUT`, `CURLE_COULDNT_CONNECT`) and `curl_error($ch)` returns a readable message. When nothing failed, `curl_error()` returns an empty string and `curl_errno()` returns `0`. - **HTTP error status**: the server answered with 4xx or 5xx. At the transfer layer this is a **success**. The manual says it directly: status codes that indicate errors, such as 404, are not regarded as failure. `curl_exec()` returns the error page body, and you must read `curl_getinfo($ch, CURLINFO_RESPONSE_CODE)` (the older alias is `CURLINFO_HTTP_CODE`). `CURLOPT_FAILONERROR => true` changes that. A status of 400 or above then becomes a transfer failure with `CURLE_HTTP_RETURNED_ERROR`, but you lose the error body, which often carries the API's explanation. Most code leaves it off and checks the status itself. ## Checking a request properly The order matters, and the comparison must be strict: 1. Call `curl_exec()` and compare the result with `=== false`. A `!$body` test misreads a legitimate empty body (`""`, as a 204 returns) as an error. 2. On `false`, read `curl_errno()` and `curl_error()` straight away, before reusing the handle. The next transfer overwrites them. 3. On success, read `CURLINFO_RESPONSE_CODE` and treat anything outside the range you expect as an application error. 4. Only then decode the body, for example with `json_decode()`. Wrapping these four steps in one small function, used by every caller, stops each call site from inventing its own partial version of the checks. ## CurlHandle since PHP 8.0, and curl_close() in 8.5 Before PHP 8.0, `curl_init()` returned a `resource`, and `curl_close()` released it. PHP 8.0 replaced the resource with the final class `CurlHandle` (also `CurlMultiHandle` and `CurlShareHandle`). Consequences: - `is_resource($ch)` is now `false` for a valid handle. Old guard code built on it silently breaks; check `$ch instanceof CurlHandle` or `=== false` instead. - The handle is freed like any object, when its last reference goes away. `curl_close()` has had no effect since 8.0, and PHP 8.5 marks it `#[\Deprecated]`, so calling it now raises an `E_DEPRECATED` notice. Let the variable go out of scope, or `unset()` it. - Type declarations can say `CurlHandle $ch`, and passing a wrong value is a `TypeError`, not a warning. ## Common mistakes - Forgetting `CURLOPT_RETURNTRANSFER` and then wondering why `$response` is `true` and the JSON appeared in the page. - Treating a returned string as proof that the call worked, without looking at the status code. - Using `if (!$response)`, which confuses an empty body with a failure. - Reading `curl_error()` as a success test: it is a message, and an empty string means no transfer error, not a 2xx status. - Keeping `curl_close()` calls in PHP 8.5 code, which now emit deprecation notices for no benefit.
- What does CURLOPT_FAILONERROR change, and why do many teams leave it off?With `CURLOPT_FAILONERROR => true`, a status of 400 or above makes `curl_exec()` return `false` with `CURLE_HTTP_RETURNED_ERROR`, so one `=== false` check catches both layers. The price is the response body: the API's error JSON, which often explains what was wrong, is discarded. Most code keeps it off and checks `CURLINFO_RESPONSE_CODE` itself.
- Why does `is_resource($ch)` return false for a working cURL handle in PHP 8?Since PHP 8.0 `curl_init()` returns a `CurlHandle` object, not a resource, so `is_resource()` is false even for a valid handle. Guards written for PHP 7 must become `$ch instanceof CurlHandle`, or a `=== false` check on the return of `curl_init()`.
- Should PHP 8.5 code still call curl_close()?No. `curl_close()` has had no effect since PHP 8.0, because the `CurlHandle` object is freed when its last reference disappears. PHP 8.5 deprecates it, so each call emits an `E_DEPRECATED` notice. Drop the call, or `unset($ch)` if you want the handle freed before the end of the scope.
saying these in an interview costs you the question
- By default, curl_exec() returns false when the server answers 404 or 500.
- Without CURLOPT_RETURNTRANSFER, curl_exec() still returns the body as a string.
- An empty curl_error() string means the request got a 2xx status.
- Checking if (!$response) is a safe failure test for curl_exec().
- is_resource() is the right way to check a cURL handle in PHP 8.
- curl_close() is required to free the handle in PHP 8.5.