skip to content

In k6, what actually cuts an iteration of the default function short, and what does not?

level: seniorimportance: must knowfreq 64%

answer

  1. reporting, not asserting
  2. the next line still runs
  3. only a throw ends the pass
  4. one iteration, not the run

basics

~20 s

In k6 a false check() cuts nothing short: it returns a boolean and the next line runs. Only a thrown error ends the iteration, and even then k6 logs it and the same virtual user starts the next pass.

solid answer

~40 s

`check()` in k6 is a reporting call, not an assertion. It returns a boolean, and a condition that evaluates false does not throw, so when a condition fails the very next statement still runs -- in a browse-then-add-to-cart iteration a failed browse check does not stop the add-to-cart request. What does end a pass early is an uncaught exception, including `fail()` from the `k6` module, which is just a wrapper over `throw`. Even then the blast radius is one iteration: k6 logs the error, counts the pass, and the **same** virtual user starts the next one, with no retry. Only `exec.test.abort()` from `k6/execution` reaches past the current iteration and stops the run. If you want a failed check to skip the rest of the pass, branch on its return value and `return`.

code

javascript · 27 lines
javascript
import http from 'k6/http';
import { check, fail, sleep } from 'k6';

const BASE = 'https://quickpizza.grafana.com';

export const options = { vus: 1, iterations: 3 };

export default function () {
  const menu = http.get(`${BASE}/`);

  // A false check changes nothing on its own - branch on it to skip the rest.
  if (!check(menu, { 'menu page loaded': (r) => r.status === 200 })) {
    console.warn('browse step failed; skipping the cart step');
    return; // ends this iteration only; the VU starts the next one
  }

  sleep(1);

  const order = http.post(
    `${BASE}/api/pizza`,
    JSON.stringify({ maxCaloriesPerSlice: 1000, mustBeVegetarian: false }),
    { headers: { 'Content-Type': 'application/json', Authorization: 'Token abcdef0123456789' } }
  );

  // fail() throws, so it ends this iteration - it does not stop the test.
  check(order, { 'pizza added to cart': (r) => r.status === 200 }) || fail('cart rejected the item');
}

go deeper

for a junior

Learn the one-line version: a failing check does not stop anything. The code after it runs, so if you need the rest of the pass skipped you must write the branch yourself.

for a middle

Explain the mechanics: check returns a boolean and a false condition does not throw, while an uncaught exception — including one raised inside a check predicate — ends the pass at the throwing line and the same virtual user then starts a new one.

for a senior

Show you can read a run with this in mind -- errors in the log with iterations still completing means per-iteration failures, and a completed run is not evidence the application behaved.

for a principal

Decide the house rule on what a script does with a failed step: continue and report, branch out of the pass, or abort the run. Inconsistency here makes two teams' results mean different things.

## `check()` does not end anything `check()` is a **reporting** call, not an assertion. It evaluates the conditions you give it, returns a plain boolean, and hands control straight back to the next statement. A `false` result changes nothing about the flow of the iteration: the line after the `check()` runs exactly as it would have if every condition had passed. That is deliberate, and it is the single biggest behavioural difference between k6's `check()` and the `assert` most people arrive with. An assertion throws. A check reports. ```javascript const menu = http.get(`${BASE}/`); check(menu, { 'menu loaded': (r) => r.status === 200 }); // returns false on a 500 sleep(1); http.post(`${BASE}/api/pizza`, body, { headers }); // still runs ``` In a browse-then-add-to-cart iteration this means a broken browse step does not stop the add-to-cart step. The `POST` goes out against a session the `GET` never established, probably fails too, and the iteration completes. If you want the second step skipped, you have to write that yourself: ```javascript if (!check(menu, { 'menu loaded': (r) => r.status === 200 })) { return; // ends this iteration; the VU starts the next one } ``` `check()` returns a boolean precisely so it can be used this way. ## What does end an iteration Only three things cut a pass short, and they differ in how far the effect reaches: 1. **An uncaught exception.** Any error thrown out of the function body -- a `TypeError` in your own code, a JSON parse failure, an HTTP error thrown because of the run's error-handling settings -- ends that iteration at the throwing line. k6 logs it, counts the pass as a completed iteration anyway, and the **same virtual user starts the next one**. The run is not stopped and no remaining iteration is skipped. 2. **`fail()` from the `k6` module.** This is a thin wrapper over `throw`, existing mostly so it can be written as `ok || fail('...')`. It behaves exactly like case 1: it ends the iteration and nothing more. Despite the name, it does not fail the run. 3. **The virtual user's run context ending.** When the test is being torn down, an in-flight iteration is interrupted rather than allowed to finish. This one is counted as an *interrupted* iteration, not a completed one. Separately, `exec.test.abort()` from `k6/execution` stops the **whole test**, not just the iteration. It is the only item on this list that reaches past the current pass. | What happens in the function | Rest of the iteration | Rest of the test | |---|---|---| | `check()` returns `false` | runs normally | runs normally | | an uncaught exception is thrown | skipped | runs normally; the VU starts its next pass | | `fail('...')` from `k6` | skipped | runs normally; the VU starts its next pass | | `return` from the function body | skipped | runs normally; this is just a normal end of pass | | `exec.test.abort()` from `k6/execution` | skipped | stopped | ## Why this trips people up The failure mode is a run that looks healthy at the process level while the application under test was broken throughout. Every iteration completed. No virtual user died. The errors were only ever *reported*, and reporting on its own never changes the shape of the run. The corollary is worth stating plainly: **a check failing is not, by itself, a verdict**. Turning failures into a run outcome is a separate mechanism entirely, and a script that only calls `check()` has not asked for one. Two further consequences follow: - **A thrown error is not fatal to the test.** People routinely expect a stack trace in the log to mean the run stopped. It does not; look at how many iterations still completed after it. - **k6 does not retry.** A pass that threw is not attempted again. The virtual user simply starts a fresh pass from the top of the function. ## Reading it back in a run When you are handed a k6 run to interpret, three questions separate these cases quickly: - Did iterations keep completing after the first error appeared? If yes, the errors were per-iteration, not fatal. - Did the number of completed iterations drop off a cliff at the end? That is usually interruption at teardown, not a script fault. - Did the run stop early with everything else still healthy? That points at a deliberate abort, not at a check or an exception.

  • Why does k6 still count an iteration that threw as a completed one?
    k6's iteration runner logs the error and then increments the completed-iteration count regardless, because the virtual user did reach the end of that pass -- abruptly, but it ended. Only a pass cut off because the run context ended is counted as interrupted instead.
  • What does `fail()` from the `k6` module actually do?
    It throws. `fail('message')` is a convenience wrapper around raising an error with that message, so it ends the current iteration exactly the way any uncaught exception does. It does not stop the test, and it does not by itself decide the run's outcome.
  • How do you make a failed check skip the rest of a k6 iteration?
    Use its return value. `check()` returns `false` when any of its conditions fails, so `if (!check(res, {...})) { return; }` ends the pass at that point. The virtual user then starts its next iteration normally; nothing else in the run is affected.

saying these in an interview costs you the question

  • Says a failed check() aborts the iteration like an assertion does
  • Thinks a thrown error in the VU function stops the whole test
  • Believes k6 retries an iteration that threw an exception
  • Uses fail() expecting it to fail the run, not the iteration
  • Expects console.error() to end an iteration or change the run
  • Reads a run as healthy because every iteration completed