skip to content

In k6, what does http.get() return when the request times out or the host is unreachable?

level: seniorimportance: should knowfreq 55%

answer

  1. a failure is still a Response
  2. status stays at zero
  3. read error and error_code together
  4. 1050 means the timeout fired
  5. throw turns it into an exception

basics

~20 s

k6 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 s

By 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 lines
javascript
import 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

for a junior

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.

for a middle

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.

for a senior

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.

for a principal

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