In k6, what happens to the running iteration when a check() condition evaluates to false?
answer
- observation, not control flow
- the iteration keeps going
- one sample per check key
- built-in checks rate metric
- only a threshold changes the verdict
basics
~10 sNothing halts. k6 records the failure on its built-in checks metric, check() returns false, and the iteration carries straight on to the next statement. The run's exit status is unaffected.
solid answer
~40 sIn k6 v2 a failed `check()` is a recorded observation, not a control-flow event. Each key in the object you pass emits one sample on the built-in `checks` Rate metric — `1` for a pass, `0` for a fail — tagged `check: <name>`, and the call returns `false` if any entry failed. It does not throw, does not skip the rest of the iteration, and does not on its own make k6 exit non-zero. So in a checkout script the condition on the order-confirmation status can fail while the same iteration goes on to request the receipt and finish normally. To turn that recorded failure into a verdict you add a threshold on the `checks` metric, for example `checks: ['rate>0.99']`.
code
javascript · 14 linesimport http from 'k6/http';
import { check } from 'k6';
export default function () {
const order = http.post('https://example.com/checkout', '{"cart":"c-42"}');
// Fails on a 500 - and changes nothing about the flow.
check(order, {
'checkout returned 201': (r) => r.status === 201,
});
// Still runs, in the same iteration, right after the failed check.
http.get('https://example.com/receipt');
}go deeper
Remember the headline: in k6 a failed check records a result and lets the iteration run on. It is not an assert, and on its own it never turns a green run red.
Be able to say what the failure actually produces - one zero-valued sample on the built-in checks Rate metric, carrying a check tag with that condition's name - and that the return value is a boolean you may branch on yourself.
Show that you treat checks as diagnostics until something binds them. Explain how you would attach a threshold to the checks metric so a failing checkout condition stops a deploy, and what a run reports when you do not.
Weigh how much of a suite's check surface should be bound at all: one global rule on the checks metric is cheap but blurs which condition broke, while per-condition rules are precise and multiply what the team has to maintain.
## Checks are observations, not assertions Every testing tool has a way of saying "this should be true". Most implement it as an **assertion**: the condition is evaluated, and if it comes out false the framework throws, the current test stops, and the run is marked failed. k6 v2 deliberately does not work that way. `check()` evaluates the condition, writes the result down, and hands control straight back to your script. Nothing is thrown, nothing is skipped, and the run's verdict is untouched. The reason is the workload k6 is built for. A load test is not a handful of cases that pass or fail; it is thousands of iterations, and one bad response in ten thousand is usually information rather than a failure. So k6 splits the two jobs apart: `check()` **measures** correctness, and a **threshold** **judges** it. ## What k6 does for each condition For every key in the object you pass as the second argument, k6 performs the same sequence, whether the condition is true or false: 1. Evaluate the condition, calling it with the first argument when it is a function. 2. Coerce whatever came back to a boolean. 3. Push exactly one sample onto the built-in **`checks`** metric — a **Rate** metric — valued `1` for a pass and `0` for a fail. 4. Tag that sample `check: <the object key>`, because `check` is one of k6's default system tags. 5. Continue to the next key. A false result sets the call's return value to `false` but does not end the loop, so every condition in the object still gets evaluated and recorded. When the last key has been processed, `check()` returns a boolean and your script resumes at the very next statement — the same statement it would have reached had every condition passed. ## What explicitly does not happen - **No exception.** A false condition is a value, not an error. There is nothing for a `try`/`catch` around the call to catch. - **No early exit from the iteration.** The lines after the `check()` call run normally. - **No change to the exit status.** k6's exit code is decided by thresholds; check results feed a metric, and a metric on its own decides nothing. - **No effect on iteration accounting.** The iteration is still counted as a full iteration and still emits `iteration_duration`. Nothing in k6's execution statistics marks it as tainted. - **No suppression of later checks.** A second `check()` call later in the same iteration behaves exactly as if the first had passed. | Behaviour on failure | Classic assertion | k6 `check()` | | --- | --- | --- | | Raises an exception | yes | no | | Stops the current unit of work | yes | no | | Marks the run failed | yes | no | | Records a measurement | usually not | yes, one sample on `checks` | | Names the failing condition | in the stack trace | in the `check` sample tag | ## The checkout iteration, concretely Take an iteration that posts a cart to a checkout endpoint, checks that the response status is `201`, and then fetches the receipt. If the checkout endpoint answers `500`, the condition is false. k6 emits a zero-valued sample tagged `check: checkout returned 201`, `check()` evaluates to `false`, and the very next line — the receipt request — is issued anyway, against an order that was never created. The iteration finishes, `iteration_duration` is recorded, the VU starts its next iteration, and `k6 run` eventually exits `0`. The failure is fully visible in the summary and completely inert as a verdict. That is not a bug; it is the contract. If you want the receipt request skipped, you write the branch yourself: `check()` gives you a boolean precisely so you can. Testing it and returning early, or calling `fail()` from the `k6` module, ends that one iteration — but that is your control flow, not the check's, and neither of them changes the run's exit status either. ## Making a failed check bind Because every check outcome lands on one metric, one threshold covers the lot: ```javascript export const options = { thresholds: { checks: ['rate>0.99'] }, }; ``` Now the failures are still recorded exactly as before, but at the end of the run k6 evaluates the rule against the `checks` rate and, if it is breached, exits non-zero. A CI step reading that exit code finally goes red. Without such a rule, no number of failed checks changes anything a pipeline can see. ## Where candidates go wrong - Describing `check()` as an assert and expecting the iteration to unwind. - Assuming k6 stops evaluating the object's remaining keys after the first false one. - Believing that a high failure percentage in the summary is itself a failing run. - Wiring CI to grep the summary text instead of adding a threshold and reading the exit code. - Forgetting that the receipt-style follow-up request in the same iteration still executes against a system state the failed check just told you is wrong.
- Does a failed k6 check() reduce the iterations count or mark the iteration incomplete?No. k6 counts it as a full iteration and emits `iteration_duration` as usual. The only trace of the failure is a zero-valued sample on the `checks` metric, its `check` tag, and the `checks_failed` row in the end-of-test summary. Nothing in k6's execution accounting distinguishes an iteration that contained a failed check.
- If check() never aborts, how do you stop a k6 iteration as soon as a check fails?Branch on the boolean the call gives back — `if (!check(res, {...})) { return; }` — or call `fail()` from the `k6` module, which throws and ends that one iteration. Both are your control flow rather than the check's, and neither changes the run's exit status.
- Do the remaining entries in a k6 check() object still run after one of them returns false?Yes. k6 walks every key in the object and emits a sample for each, aggregating only the return value at the end, so one `false` does not short-circuit the rest. A two-entry call therefore reports one pass and one fail in the summary.
A failed check is the inspector's tick box on a factory line, not the emergency stop: the item gets marked and the line keeps moving. Someone still has to write the rule that says how many bad marks end the shift.
saying these in an interview costs you the question
- Says a failed check aborts the iteration like an assert
- Believes a failed check makes k6 exit non-zero
- Thinks one false entry skips the remaining checks in the object
- Assumes checks alone are enough to gate a CI pipeline
- Expects the iteration to be dropped from the iterations count