skip to content

When a Gatling check fails, what does the default error message contain, and how do name(...) and logActualValueInError(false) change it?

level: seniorimportance: should knowfreq 36%

answer

  1. The message is assembled, not fixed
  2. Extractor, arity, validator, then outcome
  3. found nothing versus found value versus failed
  4. name replaces the whole generated prefix
  5. logActualValueInError(false) caps cardinality

basics

~20 s

With no name declared, Gatling builds the message from the extractor, the extraction arity and the validator, then appends the validator's own outcome — for example jsonPath($.orderId).find.exists, found nothing. name(...) replaces that prefix; logActualValueInError(false), added in 3.15, flattens the value-interpolating validators to failed.

solid answer

~50 s

An unnamed check is named `extractorName.arity.validatorName`, and the failure message is that name followed by **the validator's own outcome** — so the tail depends on which validator you wrote, not on a single two-case rule. With nothing captured, `exists`, `is`, `isNull`, `in` and `notNull` report `found nothing`, `lt`/`lte`/`gt`/`gte` report `can't compare nothing and <expected>`, and `optional` and `notExists` do not fail at all. On a captured-but-wrong value, `is`/`isNull`/`in` report `found <actual>`, the ordering validators report `<actual> is not greater than <expected>` and the equivalents for the other three, `notExists` and `not(x)` report `unexpectedly found <actual>`, and `notNull` reports a flat `failed`. So a bare `jsonPath("$.orderId")` fails with `jsonPath($.orderId).find.exists, found nothing`. **`name("...")`** replaces the generated prefix; **`logActualValueInError(false)`**, new in **3.15**, collapses the interpolating validators to `failed`, which is what stops a per-user value turning one error row into one row per user.

code

java · 30 lines
java
import io.gatling.javaapi.core.*;
import io.gatling.javaapi.http.*;
import static io.gatling.javaapi.core.CoreDsl.*;
import static io.gatling.javaapi.http.HttpDsl.*;

public class ErrorCardinalitySimulation extends Simulation {

  ScenarioBuilder scn = scenario("Checkout")
    .exec(http("Place order")
      .post("/orders")
      // low cardinality: keep the actual status in the message
      .check(status().is(201))
      // a pure capture: its implicit exists() can only ever report "found nothing",
      // so name it for a readable error row - logActualValueInError would be inert here
      .check(jsonPath("$.orderId")
               .name("order id present in create-order response")
               .saveAs("orderId")))
    .exec(http("Fetch order")
      .get("/orders/#{orderId}")
      // high cardinality: isEL interpolates the per-user id on a mismatch,
      // so suppress it and let those failures collapse into one "failed" row
      .check(jsonPath("$.id")
               .isEL("#{orderId}")
               .name("fetched order id matches the captured one")
               .logActualValueInError(false)));

  {
    setUp(scn.injectOpen(atOnceUsers(1)));
  }
}

go deeper

for a junior

Be ready to read a failure such as jsonPath($.orderId).find.exists, found nothing and say what each part of it means.

for a middle

Explain that the name is generated from the extractor, the extraction arity and the validator, and that name(...) replaces the whole prefix.

for a senior

Show how a per-user value in an error message explodes the report's error table, which step prevents it, and why that step does nothing on a capture-only check whose validator is the implicit exists.

for a principal

Own the suite-wide rule for which checks may log their actual value and which must be named and capped.

Gatling reports a failed check as an error message on the KO request, and those messages are what the HTML report's error table groups by. Knowing exactly how the message is assembled is what lets you keep that table readable on a run with tens of thousands of virtual users. ## How the message is assembled Gatling composes a check's display name as: ``` customName OR extractorName + "." + arity + "." + validatorName ``` and then reports the failure as that name, a comma, and the validator's own outcome string. * **`extractorName`** is the check type with its criterion, for example `jsonPath($.orderId)` or `regex(order (\d+))`. * **`arity`** is the extraction step you chose. It renders as `find`, `find(2)`, `findAll`, `count`, `findRandom` or `findRandom(3, true)`. Note that `find(0)` renders as plain `find`, so an explicit first-occurrence extraction is indistinguishable from an omitted one. * **`validatorName`** is the validation step: `exists`, `notExists`, `optional`, `is(201)`, `in(200,201)`, and the spelled-out `greaterThan(0)`, `greaterThanOrEqual(1)`, `lessThan(500)` and `lessThanOrEqual(500)` that `gt`, `gte`, `lt` and `lte` render as. The result is that **the error message spells out the steps you did not write**. A check written as nothing more than `jsonPath("$.orderId")` fails with `jsonPath($.orderId).find.exists, found nothing`, naming both the implicit `find()` and the implicit `exists()`. ## The outcome string comes from the validator There is no single two-case rule: the tail is whatever the validator you wrote returns. | validator | nothing was captured | the failing case, with logging on | |---|---|---| | `exists` (the implicit one) | `found nothing` | it has no other way to fail | | `is`, `isNull`, `in` | `found nothing` | `found <actual value>` | | `lt`, `lte`, `gt`, `gte` | `can't compare nothing and <expected>` | `<actual> is not greater than <expected>`, and the matching wording for the other three | | `notNull` | `found nothing` | `failed` — it never interpolates the value | | `notExists`, `not(x)` | no failure at all | `unexpectedly found <actual value>` | | `optional` | no failure at all | it never fails | Turning the flag off with `logActualValueInError(false)` collapses `is`, `isNull`, `in`, `notExists` and the four ordering validators to a flat `failed`. `exists`, `optional` and `notNull` are unchanged, because none of them reads the flag; `not(x)` emits `unexpectedly found <actual value>` whether the flag is on or off. Failures earlier in the chain get their own shapes — `... extraction crashed: <message>` and `... preparation crashed: <message>` — which is how you tell a malformed response apart from an honest mismatch. ## `name(...)`: replace the prefix `name(name)` takes a **static String only** and replaces the whole generated prefix. It is an optional step in its own right, sitting between validation and saving in Gatling's six-step chain. Two things it buys you: 1. A message that reads as a business fact rather than as a selector: `order id present in create-order response` instead of `jsonPath($.orderId).find.exists`. 2. Stability. Change the JSONPath expression and the generated name changes with it, splitting a previously single error row in two across runs. A declared name does not move. ## `logActualValueInError(false)`: cap the cardinality Added in **Gatling 3.15** under the heading *Controlling error messages cardinality*, this step turns off interpolating the actual value into the message. Gatling's own wording marks the boundary precisely: *most checks that apply a condition on an expected value* log the actual value when they fail. That is the whole scope — the flag reaches `is`, `isNull`, `in`, `notExists` and `lt`/`lte`/`gt`/`gte`, and no other validator reads it. Consider `jsonPath("$.id").isEL("#{orderId}")` on a follow-up request, where the expected value is per-user. With logging on, 50,000 users produce up to 50,000 distinct error strings, and the report's error table becomes one row per user. With `logActualValueInError(false)` they collapse into a single row reading `failed`. It does **not** affect `found nothing` — a value that was never captured has nothing to log — nor the ordering validators' absent branch, which reports `can't compare nothing and <expected>` either way. And it does nothing at all on a pure capture such as `jsonPath("$.orderId").saveAs("orderId")`: that check's validator is the implicit `exists()`, which ignores the flag entirely, so it is already one error row for the whole run; `name(...)` is what improves it. ## Using them together The two steps are complementary and both are cheap: * Use `name(...)` on any check whose generated name would be a long or volatile selector. * Use `logActualValueInError(false)` on any check that **compares** against an expected value where the actual varies per user — identifiers, tokens, timestamps, session values. On a capture-only check it is inert. * Keep logging **on** for low-cardinality checks such as `status().is(200)`, where `found 503` is exactly the diagnosis you want and there are only a handful of distinct values it can take. ## One subtlety about which failure you see Gatling runs **every** check on a response even after one of them fails, but reports only the **first** failure. For HTTP it also sorts checks by scope before running them — URL, then status, then header, then body, then time — regardless of the order you wrote them. So when a server returns a 500 with a JSON error payload, the reported message is the status failure, not the body failure, even if the body check appears first in your source. The body check still fails; you just do not see its message. Checks that **passed** still apply their `saveAs`, so a KO request can legitimately have written Session attributes.

  • Does `logActualValueInError(false)` change the message when the value was never found?
    No. The flag is only read by validators that interpolate an actual value — `is`, `isNull`, `in`, `notExists` and `lt`/`lte`/`gt`/`gte` — and an absent value gives them nothing to interpolate. `exists`, `is`, `in` and `notNull` report the constant `found nothing`; the ordering validators report `can't compare nothing and <expected>`. Both read the same with the flag on or off.
  • Two checks on one response both fail. Which message does the report show?
    Only the first failure, in the order Gatling ran the checks — which for HTTP is sorted by scope, not by source order: URL, status, header, body, then time. Every check still runs, and each one that passed still applies its own `saveAs`.
  • Why prefer `name(...)` over letting Gatling generate the name?
    Because the generated name embeds the selector and the extraction arity, so editing a JSONPath expression silently splits one error row into two across runs. A static name keeps the error table comparable between runs and reads as a business fact rather than a selector.

The generated name is a stack trace for the check chain: it prints the steps you never typed, in the order Gatling ran them.

saying these in an interview costs you the question

  • Thinking logActualValueInError suppresses the whole error message
  • Assuming checks report in the order they were declared
  • Expecting the actual value in a found nothing failure
  • Believing a failed check stops the remaining checks from running
  • Adding logActualValueInError(false) to a bare capture, whose implicit exists ignores it