Which k6 calls throw when placed in a script's init context, and what do they all have in common?
answer
- the VU has no state yet
- no transport, no sample channel
- http, check, group, metric add
- exec.vu throws on property access
basics
~10 sAnything needing VU state throws in k6's init context: http requests and batch, new http.CookieJar(), check(), group(), a custom metric's .add(), and exec.vu, exec.instance or exec.test.options. k6 attaches VU state only after init finishes.
solid answer
~40 sk6 builds each VU's JavaScript runtime first and attaches its **VU state** — transport, cookie jar, metric sample channel, tag set, VU and iteration numbers — only once the init context has finished. Every call k6 forbids in init tests the same thing: state is still absent. That covers `http.get()` and friends (*"Making http requests in the init context is not supported"*), `http.batch()`, `new http.CookieJar()`, `check()`, `group()`, `.add()` on a custom metric, and the `exec.vu`, `exec.instance` and `exec.test.options` accessors from `k6/execution`. Each throws a script exception rather than being skipped. The fix is always the same shape: prepare in init, act in the `default` function.
code
javascript · 16 linesimport http from 'k6/http';
import { check } from 'k6';
import { Trend } from 'k6/metrics';
const users = JSON.parse(open('./users.json')).users; // legal in init
const loginTime = new Trend('login_time', true); // legal in init
// http.get('https://example.com/health'); // throws: no VU state in init
// check(true, { ok: () => true }); // throws: no VU state in init
// loginTime.add(1); // throws: no VU state in init
export default function () {
const res = http.post('https://example.com/login', JSON.stringify(users[0]));
check(res, { 'logged in': (r) => r.status === 200 });
loginTime.add(res.timings.duration);
}go deeper
Remember the short list: no http requests, no check(), no group(), no metric .add() at module scope. Move those lines into the default function.
Explain the mechanism rather than the list: k6 attaches VU state after init finishes, and every guard tests whether that state exists yet.
Show you can read the actual error text and place the failure, including cases where the bad call is behind a condition and only fires when a later VU initialises.
Discuss what the boundary buys: a reproducible, side-effect-free init makes a script archivable and safely re-runnable across distributed instances.
## The single condition behind every rejection k6 gives each virtual user a JavaScript runtime, and attaches a **VU state** object to it only *after* the init context has finished evaluating. Every call that k6 forbids in init is guarded by the same test: if the VU state is absent, the call throws. That state carries the HTTP transport, the cookie jar, the metric sample channel, the current tag set and the VU and iteration numbers — so a call that needs any of those has nothing to work with until the VU begins iterating. That is why the rule is not an arbitrary style restriction. There is literally no transport to send a request over and no sample channel to emit a metric into while init is running. The guard is one boolean check repeated across the API surface, which is also why the error messages all read alike. ## The calls that throw, with the messages k6 prints | Call | k6's error message | |---|---| | `http.get()` / `http.post()` etc. | `Making http requests in the init context is not supported` | | `http.batch()` | `Using batch in the init context is not supported` | | `new http.CookieJar()` | `Making cookie jars in the init context is not supported` | | `check()` | `Using check() in the init context is not supported` | | `group()` | `Using group() in the init context is not supported` | | `myTrend.add(v)` | `Adding to metrics in the init context is not supported` | | `exec.vu` | `getting VU information in the init context is not supported` | | `exec.instance` | `getting instance information in the init context is not supported` | | `exec.test.options` | `getting test options in the init context is not supported` | The list continues into the other protocol modules on the same principle: opening a WebSocket, connecting a gRPC client, or invoking an RPC method all throw in init for the same reason. ## Two details that catch people out - **`exec.vu` throws on the property access itself.** k6 defines `vu`, `instance`, `scenario` and `test` as accessor properties on the `k6/execution` default export, so merely writing `const id = exec.vu;` at module scope is enough to throw — you do not have to reach `exec.vu.idInTest`. Assigning the module's default export to a variable at module scope is fine; touching one of those four properties is not. - **The metric split is a genuine inversion.** `new Trend('login_time')` is legal *only* in init, while `login_time.add(...)` is legal *only* outside it. The constructor needs the init environment that owns the metric registry; `.add()` needs the VU state that owns the sample channel. One object, two opposite placement rules, and two different error messages when you get either half wrong. ## What happens when you break the rule The guard throws a JavaScript exception rather than silently skipping the statement. Nothing is half-executed and no metric is half-emitted. Where the exception surfaces depends on when init ran: 1. If the offending line is unconditional, it fires during the load-time pass and k6 never starts the test at all — you get a script exception before any traffic leaves the machine. 2. If it is behind a condition that was false at load time, it fires later, while k6 is initializing the VU whose init takes that branch, and the run aborts then instead. Either way it is a script exception, not a soft failure, and not something a `check()` result or a threshold can absorb. That distinction matters when you read a CI log: this class of error kills the run, so there is no partial result to interpret. ## What init *can* still do The boundary is narrower than "no side effects". Init code may: - `import` and `require()` modules, and call `open()` to read local files; - read `__ENV` for variables passed with `-e VAR=value` or inherited from the system environment; - construct custom metrics with `Counter`, `Gauge`, `Rate` and `Trend` from `k6/metrics`; - write to `console`, and use `await` at the top level of the module; - do arbitrary pure computation — parsing, templating, building request bodies to reuse later. The practical fix when you hit one of these errors is almost always the same shape: keep the *preparation* in init and move the *action* into the `default` function. Parse the fixture at module scope, construct the `Trend` at module scope, then issue the request and call `.add()` per iteration. If what you actually need is a real request before the load starts — seeding an account, fetching a token — that is what `setup()` is for, because k6 runs it with a full VU state attached.
- Why can `new Trend('t')` sit in k6's init context when `t.add(1)` cannot?They test opposite conditions. The constructor needs the init environment, which owns the metric registry, so k6 rejects it outside init with *"metrics must be declared in the init context"*. `.add()` needs the VU state, which owns the sample channel, so k6 rejects it inside init. Declare once at module scope, add per iteration.
- Does reading `exec.scenario.name` work in a k6 script's init context?No. `scenario` is an accessor property like `vu`, `instance` and `test`, and its getter checks for VU state first. In init it throws *"getting scenario information outside of the VU context is not supported"*. Nothing in `k6/execution` reports live execution facts before the VU starts iterating.
saying these in an interview costs you the question
- Says the init restrictions are just a style convention
- Thinks a forbidden call in init is silently skipped
- Believes check() in init returns false instead of throwing
- Assumes exec.vu is safe as long as you do not read a property
- Thinks the ban covers all of k6/metrics, constructors included