Why does importing k6/experimental/browser abort a k6 v2 run, and what replaces it?
answer
- the old path is not simply missing
- registered, but bound to a throwing stub
- fails while loading, not mid-iteration
- the message names its own replacement
basics
~20 sThe browser module graduated to k6/browser, and k6 v2 keeps the old path registered only as a stub that throws a migration message. The run dies while the script is loading and k6 exits 107.
solid answer
~40 s`k6/experimental/browser` is no longer a module in k6 v2. The name is still in k6's module registry, but it is bound to a placeholder whose only behaviour is to throw an error saying the module has graduated and to use `k6/browser` instead. Because module instantiation happens while the script is being loaded, the failure lands before any VU runs an iteration -- the whole run dies at startup rather than one iteration failing -- and k6 exits **107**, its script-exception code. The fix is an edit to the import specifier and nothing else: graduation moved where the module is registered, not what it exports. `k6/experimental/grpc` behaves identically and points you at `k6/net/grpc`.
code
javascript · 7 lines// k6 v2: the experimental paths throw; these are the graduated core paths
import { browser } from 'k6/browser';
import grpc from 'k6/net/grpc';
export default function () {
console.log(typeof browser, typeof grpc);
}go deeper
Recognise the error on sight and know the fix is an import edit to k6/browser. The message k6 prints already names the replacement, so the skill is reading it rather than guessing.
Explain that the old name is still registered but bound to a throwing stub, and that the throw happens during script load, which is why no iteration runs and no summary appears.
Demonstrate that you would grep the script tree for the prefix rather than discovering dead paths one failed run at a time, and that you know the rewrite is not mechanical.
Consider why k6 chose a loud migration message over a silent alias, and what that implies for how your team schedules k6 major upgrades across a shared script library.
## What "graduated" means in k6 A module that starts life under `k6/experimental/` and proves itself is **graduated**: k6 re-registers it under a core import path and it joins the set of modules covered by the project's stability guarantee. The browser module went that way -- it is now `k6/browser`. Graduation is not a rewrite; it is a change of address. The exported API you get from `k6/browser` is the same one the experimental path used to hand you. The catch is that the old address does not silently keep working forever. k6 keeps the graduated path around as a deprecated alias for a while, then deletes it at the next major release. k6 v2.0.0 was that release for the browser module. ## Why you get a helpful error instead of "module not found" Deleting the path could have meant k6 simply failing to resolve it. Instead the name is still in k6's module registry -- deliberately -- bound to a stub module whose single job is to raise an error carrying a migration message: - the message states that the module **has been graduated**; - it names the replacement path, `k6/browser`; - it links the migration guide. That is why the failure is diagnosable in one read. It is also worth knowing the contrast: a path k6 has *never* registered, such as an extension path on a stock binary, produces a different error that lists the unrecognised module names and suggests a custom binary. A migration message means "this used to exist here"; an unknown-modules error means "k6 has no record of this name at all". ## Where in the run the failure happens k6 evaluates a script's init context before it starts any VU iteration, and instantiating the module graph is part of that. The stub throws at instantiation. Consequences: 1. Nothing in your `default` function ever runs, and no metric is emitted. 2. `setup()` and `teardown()` never run either -- the run does not get that far. 3. You get **one** failure per run: the load aborts at the first removed path k6 instantiates, so three dead imports in one script take three fix-and-rerun cycles if you work that way. 4. The process exits **107**, the script-exception code, which is what k6 uses for any error raised while the script is being loaded or executed. It is not the threshold code. ## The fix, and the ones that are not this simple | old path | replacement | edit | |---|---|---| | `k6/experimental/browser` | `k6/browser` | change the specifier | | `k6/experimental/grpc` | `k6/net/grpc` | change the specifier | Both of these are one-line edits, and both keep the same imported bindings -- `import { browser } from 'k6/browser'` and `import grpc from 'k6/net/grpc'`. What makes the browser case a trap is that people generalise it. A mechanical "drop the `experimental/` segment" rewrite happens to be right for the browser module and wrong for most of the rest: `k6/experimental/grpc` does **not** become `k6/grpc`, and `k6/experimental/webcrypto`, `k6/experimental/tracing` and `k6/experimental/redis` do not become `k6/webcrypto`, `k6/tracing` or `k6/redis` either. Read the message k6 prints; it names the correct destination every time. ## Reading the error in practice The whole diagnosis is three observations: - **The run failed instantly**, with no iterations and no summary metrics -- so it failed while loading, not while running. - **The message names a module path**, which narrows it to an import. - **The message names a replacement**, which means the path was registered-and-removed rather than never known. Change the specifier, rerun, and if a second dead path is hiding behind the first you will see it on the next run. If you would rather not iterate, grep the script tree for `k6/experimental/` before you run anything -- a static scan finds all of them at once, while the runtime only ever tells you about the first. ## Why so many scripts still carry the old path This is the most common first failure a team hits when it moves to k6 v2, and the reason is supply-side. The browser module lived at `k6/experimental/browser` for a long run of v0.4x and v0.5x releases, which is the era most of the copyable material was written in: tutorials, blog posts, conference examples, sample repositories and any script generated from a model trained on them. All of that names the experimental path, and none of it stops being findable when k6 ships a major. So treat a script you did not write as suspect on this point specifically. The check costs one grep, the failure costs a confusing run, and the fix is a specifier. The same reasoning applies to `k6/experimental/grpc`, which had an identical history and produces an identical failure.
- Which exit code does k6 return when a removed module import throws?107, the script-exception code. The stub throws while k6 instantiates the module graph, which happens while the script is being loaded, so the run never reaches the default function and never produces a summary.
- Does k6 report every removed import in a script at once, or one at a time?One at a time. The throw aborts the load as soon as k6 instantiates the first removed path, so a fix-and-rerun loop clears them one per run. Names k6 does not recognise at all behave differently: those are collected and reported together in a single error.
- Is the k6/browser API different from what k6/experimental/browser exported?No. Graduation moved where the module is registered, not what it exports -- the experimental path was an alias for the same code. Changing the specifier to `k6/browser` is the whole migration as far as the import is concerned.
saying these in an interview costs you the question
- Says k6/experimental/browser still works if you pass a flag
- Expects a module-not-found error rather than a migration message
- Thinks a bad import fails only the iteration that ran it
- Believes a failed import still lets the summary and exit code look clean
- Assumes k6/browser exports a different API from the old path