A k6 run's `handleSummary()` should write junit.xml, but the CI job finds no file. What do you check?
answer
- the exit code will not tell you
- check whether the hook ran
- a throw falls back silently
- disabled mode skips the call
- path opened relative to cwd
basics
~20 sCheck whether the run used --summary-mode=disabled, which skips the hook entirely; whether the hook threw, since k6 only logs that and prints the default block; and whether the returned path was writable from the run's working directory.
solid answer
~40 sStart from the fact that none of these failures change k6's exit code, so a green job proves nothing about the artifact. Four causes cover almost every case. First, `--summary-mode=disabled` or `K6_SUMMARY_MODE=disabled` suppresses summary generation, and that includes calling `handleSummary()` at all. Second, the hook threw — k6 logs the exception and falls back to the default text summary, so the console looks normal. Third, the return value was not a usable map: `undefined` gives you the default block, and a non-object gives `handleSummary() should return a map with string keys`. Fourth, the path could not be opened from the working directory k6 ran in, which logs `could not open '<path>'`. Read the run's stderr rather than its status.
code
javascript · 8 linesexport default function () {}
export function handleSummary(data) {
// renderJUnit was never imported, so this throws. k6 logs the
// exception, prints its default summary, writes no file, and
// leaves the run's exit code exactly as it was.
return { 'junit.xml': renderJUnit(data) };
}go deeper
Learn to check for the file itself rather than trusting that the command succeeded, and to look at standard error where k6 reports summary problems.
Be able to name the specific messages: an exception falling back to the default block, a non-function export, a bad return value, and a path that cannot be opened.
Show the diagnostic order and the reasoning behind it — the report runs after the verdict, so nothing here moves the exit code and the console can look entirely normal.
Decide where the artifact check belongs so that no team has to rediscover this. Summary generation can also be disabled from outside the script, which is worth governing.
## Why the job still looked green Every failure mode in the summary path is **logged, not fatal**. k6 builds the report after the test has finished and the verdict has already been decided, so a hook that throws, returns rubbish or cannot open its file leaves the process exit status exactly where it was. A pipeline that only checks whether `k6 run` succeeded will happily report success on a run that produced no artifact at all. The first move is therefore to stop trusting the status and read the run's error output. ## The checks, in order 1. **Was the hook called at all?** `--summary-mode=disabled` (or `K6_SUMMARY_MODE=disabled`) disables summary generation, and that explicitly includes the `handleSummary()` call. Because the setting lives on the command line and in the environment — there is no in-script equivalent — a wrapper script or a shared CI variable can switch it off far from the code you are reading. 2. **Did it throw?** k6 logs the exception with a stack trace and a `script exception` hint, then falls back to printing the built-in text summary. The console therefore looks completely normal, which is what makes this the most commonly missed cause. 3. **Is the export really a function?** Exporting a value rather than a function produces `exported identifier handleSummary must be a function`, and in that case k6 emits **no summary at all**. 4. **Did it return a usable map?** Returning `undefined` — easy to do by writing the object on the next line after `return`, or by forgetting the `return` in an arrow body — is treated as no result and prints the default block. A string or a number produces `handleSummary() should return a map with string keys`. 5. **Was the entry's value the right type?** A per-entry type error reads `error handling summary object <key>: invalid type ..., it needs to be a string or ArrayBuffer`. 6. **Could the path be opened?** A relative key resolves against the working directory of the k6 process, and k6 only opens the path — it does not create directories along the way. A missing parent directory or a read-only location fails that one entry with `could not open '<path>'`, collected under `Could not save some summary information:`. 7. **Did the hook run out of time?** If the call exceeded `handleSummaryTimeout` (120s by default) you get `handleSummary() execution timed out after N seconds`, and nothing it was going to return is written. ## Symptom to cause | what you observe | most likely cause | |---|---| | normal-looking text summary in the log, no file | the hook threw, or returned `undefined` | | no summary output of any kind, one error line | the export is not a function, or the return was not a map | | `could not open '...'` in stderr | the path's directory is missing or not writable from the run's cwd | | no summary and no hook-related log line at all | summary generation was disabled for the run | | an error naming a timeout in seconds | the hook exceeded `handleSummaryTimeout` | ## Reading the evidence - All of these lines go to **standard error**, so a pipeline that captures only stdout can lose them entirely. - k6 prefixes the consolidated failure with `failed to handle the end-of-test summary`; grep for it first. - Reproduce locally by running the same script and listing the directory afterwards rather than reading the console — the whole failure class is "the console lied about the file". ## A five-minute reproduction 1. Re-run with the pipeline's **exact** flags and environment rather than a clean local invocation — the cause is often a setting the script itself never sees. 2. Put a `console.log` as the first statement in the hook. If that line never appears, the call is being skipped and the answer is in check 1 or check 3, not in the hook's body. 3. Log the object you are about to return, then compare it with what actually landed on disk. 4. List the directory afterwards instead of reading the console, because the whole failure class is "the console looked fine and the file was not there". ## Making the next failure loud Since k6 will not fail the run for you, the check belongs where the artifact is consumed: assert that the file exists and is non-empty before anything downstream reads it. A one-line existence check turns a silent report-generation bug into an obvious failure, and it costs nothing on a healthy run.
- The k6 log shows `could not open 'reports/junit.xml'`. What is wrong?The path could not be opened from the process's working directory. k6 creates and truncates the file itself but does not create parent directories, so a missing `reports/` directory or a read-only location fails that entry while the rest of the run is unaffected.
- Why can a run be green while `handleSummary()` failed?The summary is produced after the test has finished and its verdict has been decided. Errors in the hook are written to the log and the exit status is left alone, so the artifact has to be checked separately.
- How would you confirm quickly that the hook is being called at all?Add a `console.log` as the first statement in the hook and rerun. If the line never appears, the call is being skipped — most often because the run used `--summary-mode=disabled` or the export is not a function.
saying these in an interview costs you the question
- Assumes a zero exit code proves the file was written
- Never considers that --summary-mode=disabled skips the hook
- Expects k6 to create missing directories in the path
- Thinks a hook exception aborts the k6 run loudly
- Looks only at stdout, where these errors never appear