skip to content

In k6, why can the built-in check() not take an async predicate, and what do you use instead?

level: seniorimportance: should knowfreq 38%

answer

  1. the built-in call has no await in it
  2. async functions are rejected outright
  3. a returned Promise object is truthy
  4. expect() ships in a jslib, not the binary

basics

~20 s

k6's built-in check() evaluates predicates synchronously and rejects any async function with an error pointing at the k6-utils jslib check. expect() is not in the k6 binary at all; it comes from the remotely imported k6-testing jslib.

solid answer

~40 s

In k6 v2 `check()` reads each entry's value and coerces it immediately — there is no await anywhere in the call. An `async` function or async arrow in the set is therefore rejected outright, with an error that names the JavaScript utils library as the replacement, and the call fails rather than passing silently. The dangerous case is the one the guard cannot see: k6 detects async functions by their constructor, so a **plain function that returns a Promise** slips through, and a Promise object is truthy — that check passes on every iteration regardless of what it resolves to. For genuinely async conditions, import the drop-in `check` from the `k6-utils` jslib, which awaits Promises and returns a `Promise<boolean>`. The Playwright-style `expect()` lives in a separate `k6-testing` jslib.

code

javascript · 15 lines
javascript
import { check } from 'k6';

export default function () {
  const res = { status: 500 };

  try {
    // rejected: "the built-in check() does not support async functions"
    check(res, { 'login ok': async (r) => r.status === 200 });
  } catch (e) {
    console.log(e.message);
  }

  // NOT rejected, and always passes: a Promise object is truthy
  console.log(check(res, { 'login ok': (r) => Promise.resolve(r.status === 200) })); // true
}

go deeper

for a junior

Remember that k6's built-in check() takes plain synchronous conditions, and that expect() is not a k6 keyword — it comes from a library you have to import by URL.

for a middle

Explain why: the call coerces each entry to a boolean immediately, so an async function is rejected with an error naming the jslib replacement.

for a senior

Know the silent case — a non-async function returning a promise passes the guard and is truthy — and how to recognise a check that has never once failed.

for a principal

Decide as a team which assertion vocabulary a suite uses; running the built-in check and an imported expect() side by side doubles the places a red result can come from.

## The built-in `check()` is synchronous, by construction k6's `check()` walks its set, produces a value for each entry, and coerces that value to a boolean immediately. There is no `await` in the call and no place to put one: the boolean must be ready before `check()` returns, because `check()` returns a boolean and not a promise. In k6 v2 the `k6` module exports exactly five names — `check`, `fail`, `group`, `randomSeed` and `sleep`. That is the entire assertion surface in the binary. Anything richer is a library you import. ## What an async predicate does k6 checks each entry before evaluating it and rejects a declared async function: ```javascript check(res, { 'login ok': async (r) => r.status === 200 }); // throws ``` The error reads *"the built-in check() does not support async functions as arguments. Use the JavaScript utils library as a replacement."* This is a loud, immediate failure, which is the good outcome — you find out at the first iteration. The detection is deliberately literal: k6 asks whether the value's constructor is `AsyncFunction`. That catches `async function () {}` and `async () => {}` and nothing else. The rejection also happens **before** anything is recorded for that entry — the call fails on inspection, not on evaluation. Entries earlier in the same set have already been evaluated and recorded normally, so what you see is a partial set of results followed by an error, not a clean failure of the whole call. ## The trap the guard does not catch A plain, non-async function that *returns* a Promise has a normal `Function` constructor, so the guard lets it through. k6 then coerces the returned value with ordinary truthiness — and **every Promise object is truthy**. ```javascript check(res, { 'login ok': (r) => fetchSomething(r).then((x) => x.ok) }); // ALWAYS passes ``` That check is green on every iteration of every run, forever, whatever the promise resolves to. It is worse than the async case in every way: - it never errors, so nothing surfaces at development time; - it never fails, so nothing surfaces during a run; - the check name is present and passing in the results, so the suite looks *more* thorough than it is. The symptom to recognise is a check that has literally never failed while its neighbours fail regularly. Any predicate whose body calls something returning a promise — with or without `.then()` — should be read as suspect. ## Where async conditions and `expect()` actually live Two separate libraries, both imported by URL rather than shipped in the binary: | you want | where it comes from | shape | |---|---|---| | a check that awaits promises | the `k6-utils` jslib `check` | drop-in replacement, returns `Promise<boolean>` | | Playwright-style assertions | the `k6-testing` jslib `expect()` | separate assertion API with retrying and soft modes | | a synchronous condition | the built-in `k6` module `check` | returns a plain `boolean` | The `k6-utils` `check` keeps the same `check(val, sets, [tags])` signature; the difference is that any promise in the set is awaited and the whole call resolves to a boolean, so you write `const ok = await check(...)` inside an `async` default function. `expect()` is the other one people expect to find built in and do not. It is a Playwright-inspired assertion API with auto-retrying and soft assertions, and it lives entirely in its own library. Reaching for `expect` without importing it is a `ReferenceError`, not a k6 feature you have not enabled. ## Choosing between them 1. **Default to the built-in `check()`.** Almost every condition worth writing about an HTTP response — a status, a header, a parsed body field — is available synchronously and needs nothing more. 2. **Reach for the jslib `check` only when the value genuinely arrives later**, which in practice means browser-module work and hand-written promise plumbing. 3. **Adopt `expect()` deliberately, as a team choice**, not per script. It is a different assertion vocabulary; mixing both across one suite means two ways to express the same condition and two places to look when something is red. Whichever you use, the guard against the silent-pass trap is the same: make sure a predicate returns a boolean, and if you cannot see at a glance that it does, it probably does not.

  • How would you spot a k6 check that silently passes because its predicate returns a Promise?
    Look for a check name that has never failed while its neighbours fail regularly, then read the predicate: if its body calls anything returning a promise, or uses `.then()`, the entry is a Promise object and therefore truthy. Rewrite it against the jslib `check` with `await`.
  • Is expect() available in a k6 script without an import?
    No. The k6 binary's `k6` module exports only `check`, `fail`, `group`, `randomSeed` and `sleep`. `expect()` belongs to the separate k6-testing library, imported by URL; using the bare name without that import is a `ReferenceError`.
  • What does the jslib check give you that k6's built-in check() does not?
    It awaits promises. Any entry that is a promise, or an async function, is resolved before being judged, and the call itself returns a `Promise<boolean>` you await. The signature is otherwise the same, which is why it is described as a drop-in replacement.

It is the difference between a form that rejects an unsigned page and one that accepts a sealed envelope as a signature: the first tells you immediately, the second is filed as complete forever.

saying these in an interview costs you the question

  • Thinks the built-in check() awaits an async predicate
  • Expects expect() to be available without an import
  • Believes a promise-returning predicate is evaluated
  • Assumes a truthy Promise object means the check passed
  • Mixes two assertion libraries across one suite by accident