skip to content

What happens to k6's default end-of-test summary when a script exports `handleSummary()`?

level: middleimportance: should knowfreq 58%

answer

  1. the default is just a fallback entry
  2. exporting replaces the console block
  3. empty object means total silence
  4. throwing falls back to the default
  5. non-function export means no summary

basics

~20 s

k6 stops printing it. The default text block is only what k6 emits when no hook is exported, so an exported handleSummary replaces it entirely, and returning an empty object leaves the run with no report at all.

solid answer

~40 s

Exporting `handleSummary()` suppresses k6's built-in text block. When no hook is present, k6 internally produces `{ stdout: <the text summary> }` and processes it through exactly the same map machinery, so your hook simply takes that job over: `return { stdout: myText }` replaces the block, and `return {}` silences the end-of-test report completely. Two fallbacks matter. If the hook returns `undefined` or `null`, k6 treats it as no result and prints the default text summary anyway. If the hook **throws**, k6 logs the exception, still prints the default summary, and leaves the run's exit code unchanged — so a broken report generator looks like a passing job. If the export is not a function at all, k6 logs an error and emits no summary.

code

javascript · 6 lines
javascript
export default function () {}

// Writes the file and prints nothing at all.
export function handleSummary(data) {
  return { 'junit.xml': '<testsuites/>' };
}

go deeper

for a junior

Know that once the script exports handleSummary, the familiar text block stops appearing unless you return it yourself under the stdout key.

for a middle

Explain the fallback rule: k6 substitutes its own stdout entry only when the hook returns nothing usable, which is why an empty object produces total silence.

for a senior

Point out that every failure in this path is logged rather than fatal, so a silently broken report generator has to be caught by checking for the artifact, not by the run's status.

for a principal

Consider who is allowed to set --summary-mode. It sits outside the script, so a command-line or environment change can switch off reporting for every script an organisation runs.

## Where the default block actually comes from k6's familiar end-of-test text block is not a hard-coded print statement sitting beside the hook. At the end of a run k6 builds the summary object, calls your `handleSummary()` if you exported one, and then passes the result through a wrapper. The wrapper's rule is short: **if the hook produced nothing usable, substitute `{ stdout: <rendered text summary> }`**. Everything then goes through the same delivery path — reserved keys to the streams, other keys to files. That single fact explains every behaviour on this page. The default summary is a fallback entry, not a separate feature, so exporting a hook that returns a real map removes it. ## What your hook replaces, case by case | what the script does | what k6 emits at the end of the run | |---|---| | exports no `handleSummary` | the default text summary on `stdout` | | returns `{ stdout: myText }` | your text on `stdout`, and nothing else | | returns `{ 'junit.xml': xml }` | the file only — **no console block at all** | | returns `{}` | nothing: no console output, no file | | returns `undefined` or `null` | the default text summary on `stdout` | | throws an exception | an error in the log, then the default text summary | | exports `handleSummary` as a non-function | an error in the log, and no summary of any kind | The row people trip over is the third. A script that writes only a file goes quiet in the terminal, and on a CI job that reads the console for evidence, that reads as "the test produced nothing". ## Failures here are logged, never fatal - A thrown exception inside the hook is reported at error level with a `script exception` hint and a stack trace, and then k6 **falls back** to the default text summary. - A non-function export produces `exported identifier handleSummary must be a function`. - A non-map return produces `handleSummary() should return a map with string keys`. - Whatever went wrong, k6 logs `failed to handle the end-of-test summary` and moves on. - **None of this changes the process's exit status.** The report is a delivery step; it does not carry a verdict. ## `--summary-mode` can remove the hook entirely The summary block has three modes, selected with `--summary-mode` or `K6_SUMMARY_MODE`: 1. `compact` — the default; thresholds, checks and aggregated metrics by category. 2. `full` — everything compact shows, plus per-group and per-scenario detail. 3. `disabled` — **no summary generation at all**, which includes not calling `handleSummary()`. That third value is a real trap: the option lives outside the script, so someone can switch off your file-writing hook from the command line or an environment variable without touching the code. Note also the asymmetry — the summary mode is a CLI/env setting with **no in-script equivalent**, so you cannot pin it from the exported `options` object. ## Keeping the default text as well as your own Because the built-in renderer is internal to the binary rather than an importable k6 module, a script that wants both its own artifact and the familiar console block calls the community `textSummary()` helper published in the k6 jslib and returns its output under `stdout` alongside the file key. The point for this topic is the shape of the return, not the helper: two keys, two destinations, one call. ```javascript export function handleSummary(data) { return { 'junit.xml': renderJUnit(data), stdout: renderText(data), }; } ``` ## The report is delivery, not verdict - The hook runs after the test has finished, so nothing it does or fails to do changes what the run decided. - Every error in this path — a throw, a return of the wrong shape, an unwritable path — is logged and then dropped. - That is convenient, because a broken report never breaks a test, and dangerous, because a broken report never announces itself either. - If the artifact matters to something downstream, the check that it exists belongs downstream, not in k6. ## Version notes In k6 v2 the way to turn the report off is `--summary-mode=disabled`, and an unrecognised value for that setting is rejected before the test starts rather than at the end of it. The old `--no-summary` flag and the `legacy` summary mode were both removed in v2.0.0, so a pipeline that still passes `--no-summary` fails on an unknown flag rather than quietly working.

  • If `handleSummary()` throws, does the k6 run fail?
    No. k6 logs the exception with a stack trace, falls back to printing the default text summary, and leaves the exit code untouched. A report generator that has been broken for weeks can sit in a green pipeline unnoticed.
  • How do you turn the end-of-test summary off entirely in k6 v2?
    `--summary-mode=disabled`, or `K6_SUMMARY_MODE=disabled`. It suppresses summary generation altogether, including the call to `handleSummary()`. The older `--no-summary` flag and the `legacy` mode were removed in v2.0.0.

saying these in an interview costs you the question

  • Thinks the default block still prints alongside your output
  • Believes returning an empty object keeps the default summary
  • Thinks a throwing handleSummary fails the run
  • Expects --no-summary to still exist in k6 v2
  • Assumes --summary-mode can be set in the script options