skip to content

In k6, does calling fail() from the k6 module fail the whole test run?

level: middleimportance: should knowfreq 41%

answer

  1. it is just a throw
  2. one iteration, not the run
  3. logged, then the next iteration starts
  4. writes no metric of its own
  5. abort() is the whole-test version

basics

~10 s

No. k6's fail() throws an error that ends only the current iteration. k6 logs it, still counts the iteration as finished, starts the next one, and leaves the run's exit status untouched.

solid answer

~40 s

`fail()` is one of the five functions the `k6` module exports in k6 v2, alongside `check`, `group`, `randomSeed` and `sleep`, and it is a thin wrapper on `throw`: it raises an error carrying the message you pass. The exception unwinds the current iteration, k6's executor logs it at error level, counts the iteration as a full iteration, and immediately starts the next one. Nothing about the verdict changes — `fail()` writes no metric and the exit status stays whatever the thresholds decide. So `if (!check(order, {...})) fail('checkout did not return 201')` is a way to skip the rest of *this* checkout iteration, not a way to fail the build. To stop the whole test deliberately, k6 offers `exec.test.abort()` from `k6/execution`.

code

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

export default function () {
  const order = http.post('https://example.com/checkout', '{"cart":"c-42"}');

  if (!check(order, { 'checkout returned 201': (r) => r.status === 201 })) {
    // Ends THIS iteration only; the next one starts immediately.
    fail('checkout did not return 201');
  }

  http.get('https://example.com/receipt');
}

go deeper

for a junior

Remember that fail() in k6 is a convenience wrapper on throw. It stops the iteration you are in and nothing more; the test keeps running and still exits zero.

for a middle

Explain the executor's handling: the error is logged, the iteration is still counted as a full iteration, and the next one starts at once. Contrast that with exec.test.abort(), which ends the test.

for a senior

Show when the guard earns its place - skipping follow-up requests that would otherwise pollute http_reqs and the duration metrics with responses to an operation that never happened - and pair it with a threshold so the run still gets a verdict.

for a principal

Consider what a suite full of fail() guards costs in log volume and diagnosability at scale, and whether the team is better served by recording the condition and letting the iteration finish for comparable measurements.

## What `fail()` actually is The `k6` module in k6 v2 exports exactly five things: `check`, `fail`, `group`, `randomSeed` and `sleep`. `fail()` is the smallest of them. It takes an optional message string and does one thing — it raises an error carrying that message. It is a convenience wrapper on JavaScript's `throw`, and it exists mainly because `throw` is a statement and therefore cannot be written on the right of `||`, whereas `someCondition || fail('...')` reads naturally in test code. Because it is a `throw`, its blast radius is exactly the blast radius of any uncaught exception in a k6 iteration: **the current iteration, and nothing else**. ## What happens when the exception escapes When an error propagates out of the exported default function, k6's executor catches it and: 1. Checks whether the run is being torn down. If not, it formats the error and logs it at **error** level, with the script stack trace. 2. Adds one to the **full iteration** count. The iteration is not marked incomplete, not retried, and not subtracted from anything. 3. Returns control to the executor, which immediately starts the VU's next iteration according to whatever scenario is driving it. Nothing in that sequence touches the run's verdict. `k6 run` exits with whatever the thresholds decide, and if there are no thresholds it exits `0` — after logging one error line per failed iteration. ## The distinction that gets tested | Call | Ends | Changes the run's exit status | | --- | --- | --- | | `check()` returning `false` | nothing | no | | `fail()` from `k6` | the current iteration | no | | a bare `throw` | the current iteration | no | | `exec.test.abort()` from `k6/execution` | the whole test | yes | `fail()` and `exec.test.abort()` are the two calls people conflate, and the interview question is usually a probe for exactly that. `fail()` is control flow inside one iteration; `exec.test.abort()` tears the test down. If what you want is "stop everything, this run is meaningless", `exec.test.abort()` is the tool. If what you want is "this iteration cannot usefully continue", `fail()` is. ## The checkout iteration The idiomatic use is guarding the rest of an iteration on a check result. `check()` hands back a boolean precisely so you can branch on it: - The checkout POST returns `500`. - `check(order, { 'checkout returned 201': ... })` records the failure on the `checks` metric and returns `false`. - Your `if (!...)` sees that and calls `fail('checkout did not return 201')`. - The throw unwinds; the receipt request that would have followed never runs; k6 logs the message and starts the next iteration. Notice what is and is not accomplished. You have stopped a pointless request against an order that does not exist — genuinely useful. You have **not** made the run fail. The `checks` metric records the same zero-valued sample it would have recorded anyway, `fail()` itself writes no metric of its own, and the exit status is untouched. ## Things `fail()` does not do - It does not emit a sample on `checks` or on any other metric. Only `check()` writes check samples; if you call `fail()` without a preceding `check()`, the failure exists only in the log. - It does not mark the iteration as dropped or interrupted. Those counters are for VU starvation and for iterations cancelled by a graceful stop, not for script errors. - It does not stop other VUs. Every other VU carries on through its own iterations, unaware. - It does not accumulate. A thousand failed iterations produce a thousand log lines and the same exit code as zero of them. - It does not skip `teardown`. That function runs once at the end of the test regardless of how many iterations ended early. ## How to use it well Reach for `fail()` when continuing would produce misleading measurements — requesting a receipt for an order that was never created inflates `http_reqs` and pollutes `http_req_duration` with a fast 404. Pair it with a threshold on `checks` so the run still gets a verdict, and keep the message specific enough to identify the condition in a log of thousands of iterations. If you want the run itself to stop the moment the situation is hopeless, that is a different call and a different intent.

  • What does k6 print when fail() ends an iteration, and does the run keep going?
    The executor formats the error, logs it at error level with the script stack trace, adds one to the full-iteration count and immediately starts the next iteration. `k6 run` finishes normally, and the exit status is still decided by thresholds alone.
  • How do you deliberately abort an entire k6 test from inside the default function?
    Import `k6/execution` and call `exec.test.abort()`, optionally with a reason. Unlike `fail()`, that tears the whole test down rather than one iteration. `fail()` and a bare uncaught `throw` are equivalent in k6 — both end only the iteration they occur in.

saying these in an interview costs you the question

  • Says fail() aborts the whole k6 test run
  • Thinks fail() makes k6 exit non-zero on its own
  • Believes the interrupted iteration is not counted
  • Confuses fail() with exec.test.abort() from k6/execution
  • Assumes fail() writes a sample to the checks metric