skip to content

How does k6's own require() differ from an ES import statement in a test script?

level: middleimportance: must knowfreq 51%

answer

  1. both exist, same loader underneath
  2. not Node's require
  3. init stage only
  4. computed specifier versus literal
  5. one syntax per file

basics

~20 s

k6 accepts both, but require() is k6's own implementation, not Node's: it loads built-in k6 modules, local files and remote https scripts only, it exists solely in the init stage, and it takes a specifier computed at runtime.

solid answer

~40 s

Both forms work in k6, and both go through the same loader, so neither gains you Node's resolution rules. The differences are k6's own. `require()` is a global that k6 installs **only in the init stage**; calling it inside the default function fails with `the "require" function is only available in the init stage`. Because it is an ordinary function call it accepts a computed string, which is why `require(import.meta.resolve('./helpers.js'))` is a usable pattern, while an `import` specifier must be a literal. k6 also decides per file which system a module is written in: if the parsed source has any `import` or `export` entries, or top-level `await`, k6 treats it as an ES module, otherwise it wraps it as CommonJS. Mixing both syntaxes inside one file throws.

code

javascript · 6 lines
javascript
// smoke.js -- ES module form, the default choice
import { login } from './helpers.js';

export default function () {
  login();
}

go deeper

for a junior

Know that k6 accepts both styles and that new k6 scripts are normally written with import and export. Neither style lets you drop the file extension or import a package by bare name.

for a middle

Explain that k6's require() is its own implementation, usable only in the init stage, and that it accepts a specifier computed at run time. Say how k6 classifies a file as ESM or CommonJS by what the parsed source contains.

for a senior

Diagnose the mixing error from its text: a stray module.exports in a file that k6 classified as an ES module. Show that you would standardise on one syntax across a shared helper library rather than debug the boundary case repeatedly.

for a principal

The judgment call is uniformity. Committing a suite to ES module syntax makes the dependency graph static and reviewable; permitting require() buys dynamic specifiers for a handful of cases and costs you a rule that every contributor has to remember.

## Both are supported, and both use the same loader k6 accepts ES module syntax (`import` / `export`) and CommonJS syntax (`require()` / `module.exports`) in the same test suite. What matters is that **neither changes how a specifier is resolved**. Both are handed to the same k6 loader, so both are subject to the same rules: an explicit file extension, no `node_modules` lookup, `k6` and `k6/…` served from the built-in registry, and `https://` for remote files. Choosing `require()` does not buy you any of Node's behaviour. The k6 documentation is explicit about this: `require()` is a custom k6 implementation of module loading that handles built-in k6 modules, scripts on the local filesystem and remote scripts over HTTPS, and does **not** support the Node.js module resolution algorithm. ## The differences that are real | Property | `import` in k6 | k6's `require()` | |---|---|---| | Where it may appear | Top level of a module, hoisted | Only in the init stage (the global scope) | | Specifier form | A literal string, fixed at parse time | Any expression evaluating to a string | | Evaluation order | Dependencies linked and evaluated before the importing module's body | Runs at the point the call is reached | | Failure outside the init stage | Not applicable | Throws `the "require" function is only available in the init stage` | That last row is the one that surprises people. `require()` is installed on the k6 runtime as a global whose implementation first checks whether the virtual user has entered its running state; if it has, the call is rejected with a pointer to the k6 test-lifecycle documentation. So you cannot lazily pull in a helper module from inside the default function. The computed-specifier row is why k6 exposes `import.meta.resolve()`. It returns the absolute URL that a given specifier resolves to **from the file the call is written in**, and its result is a plain string, so it composes with `require()`: ```javascript // lib/helpers.js const config = require(import.meta.resolve('./config.js')); ``` ## How k6 decides which system a file is written in k6 does not use the file extension or a manifest to classify a module. It parses the source and asks one question: does the program have any export entries, any import entries, or top-level `await`? If yes, the file is an ES module. If not, k6 wraps it as CommonJS and gives it `module` and `exports`. The practical consequences: - A `helpers.js` written with `module.exports = { login }` can be pulled in by `import { login } from './helpers.js'`: k6 exposes each key of `module.exports` as a named export, and a default import receives the whole `module.exports` object unless the file set `exports.default` itself. - A `helpers.js` written with `export function login()` can be pulled in by `require('./helpers.js')`, and its named exports appear as properties of the returned object. - A file that contains nothing but statements — no imports, no exports, no top-level `await` — is treated as CommonJS, which is why plain bundler output loads cleanly. So the two styles interoperate **across** files. The line k6 draws is inside a single file. ## Mixing the two syntaxes in one file k6 defines `module` and `exports` on its global object as accessor properties that throw as soon as they are touched: ``` you are trying to access identifier "module", this likely is due to mixing ECMAScript Modules (ESM) and CommonJS syntax. This isn't supported in the JavaScript standard, please use only one or the other ``` The mechanism explains the wording. When a file is classified as CommonJS, k6 wraps it and passes `module` and `exports` in as wrapper parameters, so the throwing globals are shadowed and never reached. When a file is classified as an ES module there is no wrapper, so a stray `module.exports = …` hits the global accessor and blows up. Adding a single `export` statement to a CommonJS helper flips its classification and can break the rest of the file for exactly this reason. ## Choosing one for a shared helper file For a `helpers.js` imported by three scripts, prefer ES module syntax and be consistent: 1. Write the helper with `export` and import it with `import`, so the dependency is visible statically and k6 can link the whole graph before anything runs. 2. Reach for `require()` only when the specifier genuinely has to be computed, and pair it with `import.meta.resolve()` so the path stays relative to the file it is written in. 3. Never put `module.exports` and `export` in the same file — k6 will reject it, and the error names the identifier rather than the line that caused the mix.

  • Can require() be called inside the default function to load a module lazily?
    No. k6 installs `require()` as a global that checks whether the virtual user has left the init stage, and rejects the call with `the "require" function is only available in the init stage (i.e. the global scope)`. Every module a run needs has to be pulled in while the script's top level is executing.
  • How does k6 decide whether helpers.js is an ES module or CommonJS?
    By parsing it. If the program has any import entries, any export entries, or top-level `await`, k6 treats it as an ES module; otherwise it wraps it as CommonJS and supplies `module` and `exports`. The filename and any surrounding package manifest play no part in the decision.
  • What is import.meta.resolve() for in a k6 script?
    It turns a specifier into the absolute URL it would resolve to from the file the call appears in, and returns it as a string. That makes it the safe way to build an argument for `require()`, and k6 also recommends it for `open()`, whose current relativity differs from that of imports.

saying these in an interview costs you the question

  • k6's require() is Node's require()
  • require() lets you skip the file extension
  • you can require a module inside the default function
  • module.exports and export can share one file
  • require() searches node_modules but import does not