In k6, what does http.get() return when the request times out or the host is unreachable?
answer
- a failure is still a Response
- status stays at zero
- read error and error_code together
- 1050 means the timeout fired
- throw turns it into an exception
basics
~20 sk6 returns a Response object rather than throwing. res.status is 0, res.body is null, and res.error and res.error_code carry the reason -- 1050 for a timeout, 1212 for a refused connection. A warning is logged and the iteration continues.
solid answer
~40 sBy default a transport failure in k6 is **not** an exception. `http.get()` still returns a `Response`, but one where `res.status` is `0` because no HTTP response ever arrived, `res.body` is `null`, and the diagnosis lives in `res.error` (a message string) and `res.error_code` (a number). k6 logs `Request Failed` as a warning and the iteration carries on to the next line, which is why a script that only reads `res.json()` blows up on a confusing null-body error instead of the real cause. The numeric codes are grouped: `1050` request timeout, `1101` no such host, `1211` dial timeout, `1212` connection refused, `1310` unknown certificate authority, and `1400`-`1599` mirroring HTTP 4xx and 5xx statuses. Setting the `throw` option makes k6 raise instead.
code
javascript · 14 linesimport http from 'k6/http';
export default function () {
const res = http.post('https://api.example.com/orders', JSON.stringify({ sku: 'A-19' }), {
headers: { 'Content-Type': 'application/json' },
timeout: '2s',
});
if (res.status === 0) {
console.error(`transport failure ${res.error_code}: ${res.error}`);
return; // res.body is null here, so do not parse it
}
console.log(res.json('id'));
}go deeper
Remember that a failed k6 request does not throw. You still get a Response back, and res.status of 0 is the signal that no reply arrived at all.
Explain the fields that carry the diagnosis -- res.error and res.error_code -- and the code ranges: 1050 for timeout, 1100s for DNS, 1200s for TCP, 1300s for TLS, and 1400 to 1599 echoing HTTP statuses.
Show how you would triage a run full of status 0 responses: group by error_code, separate DNS from refused connections from TLS, and tighten the per-request timeout so stalls surface as countable failures rather than held VUs.
Decide the team's posture on the throw option and on timeout defaults. Throwing surfaces bugs fast in development but truncates production-shaped runs, and a single script-wide timeout rarely fits endpoints with genuinely different service expectations.
## A failure is still a Response This is the fact that surprises people coming from a language where a network error is an exception. In k6, when a request cannot complete -- DNS does not resolve, the connection is refused, TLS verification fails, or the request exceeds its timeout -- `http.get()` and every other `k6/http` call **return normally**. What you get back is a `Response` object describing the failure rather than a reply. - `res.status` is `0`. There was no HTTP response, so there is no status code to report. - `res.body` is `null`, which means `res.json()` and `res.html()` throw if you call them. - `res.error` holds a message string such as `dial: connection refused` or `request timeout`. - `res.error_code` holds a number identifying the failure class. - k6 logs `Request Failed` at warning level with the error attached. Execution then continues on the next line of the iteration. Nothing aborts, and the test's exit code is not affected by this alone. ## The error code ranges `res.error_code` is grouped so that whole classes of failure can be recognised without matching on message text. The same value is also attached to the request's samples as an `error_code` tag. | Range | Class | Examples | |---|---|---| | 1000-1099 | general | `1020` invalid URL, `1050` request timeout | | 1100-1199 | DNS | `1101` no such host | | 1200-1299 | TCP | `1211` dial timeout, `1212` connection refused, `1220` reset by peer | | 1300-1399 | TLS | `1310` unknown authority, `1311` certificate does not match hostname | | 1400-1599 | HTTP status echoes | `1404` for a 404, `1500` for a 500 | | 1600-1699 | HTTP/2 | GoAway, stream and connection errors | The `1400`-`1599` band is worth calling out separately: those are **not** transport failures. A `404` arrives as a perfectly good HTTP response with `res.status` of `404`, and k6 additionally sets `res.error_code` to `1000 + status`. So `res.error_code` being non-zero does not by itself mean the request failed to reach the server. ## The timeout that is doing this Every request carries a `timeout`, defaulting to **60 seconds**, and you override it per request through params. It accepts a duration string or a plain number read as milliseconds, so `{ timeout: '2s' }` and `{ timeout: 2000 }` are the same thing. When it fires you get `res.status` `0` and `res.error_code` `1050`. Sixty seconds is a very long time to wait during a load test, and leaving it at the default is how a small number of stuck requests quietly hold VUs hostage. ## Reading a failure without being misled 1. Assert on `res.status` explicitly rather than assuming a truthy `res` means success -- the object is always truthy. 2. Log `res.error_code` alongside `res.error`; the code is stable and greppable, the message is not. 3. Do not reach for `res.json()` before you have confirmed a status, or the null body produces `the body is null so we can't transform it to JSON`, which hides the real DNS or connection error underneath. 4. Remember the response-callback consequence: status `0` sits outside the default expected band of `200`-`399`, so a transport failure also lands as a failure in `http_req_failed`. ## What is still readable on a failed Response Not every field goes dark, and knowing which survive saves a lot of guessing at the console. - `res.request` is fully populated -- `method`, `url`, `body`, `headers` and `cookies` describe exactly what k6 tried to send, which is how you confirm the target and payload were what you meant. - `res.timings` is populated with whatever phases completed before the failure, so a timeout still shows where the time went. - `res.headers`, `res.status_text`, `res.cookies` and `res.tls_version` are empty, because they can only come from a reply that never arrived. - `res.url` holds the URL that was requested rather than a final redirected one. ## Turning failures into exceptions The `throw` option flips this behaviour. With it on, a failing request raises a JavaScript exception instead of returning a `Response`, so the iteration ends there and the stack points at the call. It is a debugging posture rather than a load-test posture: in a real run you usually want the failed request recorded and the iteration continuing, because stopping on the first refused connection tells you far less than a full run's worth of failure codes. For a batch, the same rules apply per entry. With `throw` off, an entry that fails to parse or connect leaves a `Response` in its slot with `error` and `error_code` populated, k6 logs `A batch request failed`, and the other entries' results are still returned. All of the above is k6 v2.x behaviour.
- In k6, why does a 404 response have a non-zero res.error_code?k6 echoes HTTP error statuses into the code space as `1000 + status`, so a `404` becomes `1404` and a `500` becomes `1500`. That response arrived normally -- `res.status` is `404` and the body is present -- so a non-zero `error_code` alone never proves the request failed to reach the server.
- What does k6's default 60-second request timeout cost you in a load test?A VU waiting on a stuck request is a VU doing nothing for up to a minute, which drains concurrency and stretches iteration durations. Setting a `timeout` in each request's params that reflects the endpoint's real service expectation converts a silent stall into a fast, countable `1050` failure.
Like a courier returning your parcel with a reason slip attached instead of simply vanishing: you still get something back, and the slip -- not the absence -- tells you what went wrong.
saying these in an interview costs you the question
- Wrapping k6 requests in try/catch expecting network errors to throw
- Treating a truthy Response object as proof the request succeeded
- Calling res.json() before checking that res.status is non-zero
- Reading res.error_code as an HTTP status code
- Leaving the 60-second default timeout on every request in a load test