Why should a REST Assured polling loop not use then().statusCode(200) as its loop condition?
answer
- validation throws, it does not return
- the catch is the whole problem
- a 500 reads as still not ready
- extract inside, expect once outside
basics
~20 sBecause REST Assured validation is not a boolean: a failed expectation throws AssertionError. Using it as a loop condition means catching AssertionError every iteration, which silently swallows real failures as not-ready-yet and ends with a timeout message naming nothing.
solid answer
~40 sREST Assured signals a failed expectation by throwing `AssertionError` from its internal response specification, which collects the failures, builds a message and throws. There is no boolean form. So the only way to make `then().statusCode(200)` a loop predicate is `try { ... } catch (AssertionError e) { keep waiting; }`, and that catch cannot tell "the tram inspection is still `PENDING`" from "`GET /inspections/{inspectionId}` returned 500", "the field was renamed" or "the schema changed". Every genuine defect becomes a slow timeout whose message names your deadline instead of the mismatch. Shape the loop the other way round: inside it validate only what is true on every attempt, read the changing value with `extract().path("status")` and compare it in plain Java, then put the real expectations in one validating call after the loop.
code
java · 18 linesimport static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.equalTo;
// WRONG: catch (AssertionError) cannot tell PENDING from 500 or a renamed field
String status = "PENDING";
long deadline = System.currentTimeMillis() + 20_000L;
while ("PENDING".equals(status) && System.currentTimeMillis() < deadline) {
Thread.sleep(500L);
status = given().spec(fares).pathParam("inspectionId", inspectionId)
.when().get("/inspections/{inspectionId}")
.then().statusCode(200) // an invariant, true on every attempt
.extract().path("status"); // the read, not an expectation
}
given().spec(fares).pathParam("inspectionId", inspectionId)
.when().get("/inspections/{inspectionId}")
.then().body("status", equalTo("SETTLED"));go deeper
Know that a failed REST Assured expectation ends the call by throwing rather than by returning a false value. That one fact is what makes a validating chain unusable as a loop condition.
Explain the mechanics: validation throws AssertionError from the response specification, so a polling loop built on it needs a catch, and that catch cannot distinguish not-ready-yet from genuinely broken.
Show the loop you would approve in review - invariants and a read inside, one validating call outside - and describe the failure message each shape produces when the service returns 500.
Own the reporting consequence across the suite: a catch-and-continue wait turns every defect into a timeout. Decide what a shared wait helper is allowed to swallow and what it must always let through.
## How a REST Assured expectation fails `then()` does not return a verdict. It returns a `ValidatableResponse` you can chain more expectations onto, and the expectations themselves are accumulated and checked. When any of them fails, REST Assured's internal response specification assembles a message — how many expectations failed, what each one expected, what it got, plus anything you supplied through `onFailMessage(...)` — and throws an `AssertionError` carrying it. That single design decision is why a validating call cannot be a loop condition. There is no boolean overload and no result object to inspect. The only way to keep going after a failure is to catch the throwable. ## What the catch actually swallows Write the tempting loop against a tram fare-inspection and you get this: ```java boolean settled = false; while (!settled && System.currentTimeMillis() < deadline) { try { given().spec(fares).pathParam("inspectionId", inspectionId) .when().get("/inspections/{inspectionId}") .then().statusCode(200) .body("status", equalTo("SETTLED")); settled = true; } catch (AssertionError notYet) { Thread.sleep(500L); } } ``` Every one of the following throws the same `AssertionError` and lands in the same catch: - the inspection is genuinely still `PENDING` — the one case you meant to handle; - the endpoint answered `500` because the fare engine fell over; - the endpoint answered `404` because the `inspectionId` was never persisted; - the field was renamed from `status` to `settlementStatus`, so the path resolves to nothing; - the response is now `text/html` from a proxy error page rather than JSON; - your matcher was wrong all along and would never have passed. The loop treats all six as "not ready yet", waits out the deadline, and then fails with whatever your timeout code says. The report names the clock. The defect is invisible. ## The shape that survives review Invert which half of the call lives inside the loop. 1. **Inside the loop, validate only invariants.** On a status resource, `then().statusCode(200)` is true whether the inspection is `PENDING` or `SETTLED`, so keeping it inside is not just harmless — it is valuable, because a real `500` now blows up immediately instead of being read as patience. 2. **Inside the loop, read rather than assert.** `extract().path("status")` returns the value as an ordinary Java object. Compare it in the `while` condition. Nothing in that comparison can throw. 3. **Outside the loop, assert once.** A final `then().body("status", equalTo("SETTLED"))` produces the real failure message — the path, the matcher and the actual value — and that is what the report carries. The difference in a red build is stark: | Loop shape | Failure message when the endpoint 500s | |---|---| | Expectation inside, `AssertionError` caught | "timed out after 20s waiting for SETTLED" | | Invariant inside, expectation outside | "expected status code 200 but was 500" | ## The narrower judgement call There is one legitimate catch, and it is not `AssertionError`. If the resource does not exist yet at all — the tram inspection has been accepted but `GET /inspections/{inspectionId}` still answers `404` for a moment — then a `404` genuinely is a not-ready signal for this API, and you should express that as data rather than as a caught throwable: - extract the status code with `extract().statusCode()` and branch on it in plain Java; - or use `then().statusCode(anyOf(is(200), is(404)))` inside the loop so both are invariants, and then read the body only when the code was 200. Both keep the "not ready" case explicit and leave every other failure free to escape immediately. Neither requires you to catch anything. ## Why this bites harder than it looks Suites that poll this way tend to look healthy for a long time, because a catch-and-continue loop converts intermittent server errors into slow passes whenever the next attempt succeeds. What you lose is the evidence: nobody ever sees the `500` that happened on attempt three, so nobody investigates it. The failure surfaces months later as "this test is slow and flaky" rather than as "the fare engine returns 500 about one call in forty". The rule worth enforcing in review is short: **no wait helper catches `AssertionError`.** If a helper needs to tolerate a condition, it tolerates it as an explicit value it read, not as an exception type that covers everything the library can complain about.
- What should the failure look like when the tram inspection never settles at all?It should fail on the value you were waiting for, not on the clock alone. After the loop exits, run one validating call so `then().body("status", equalTo("SETTLED"))` puts the actual status, the path and the matcher into the report. A bare "timed out after 20 seconds" names the deadline and hides whatever the service was really doing.
- Is there any REST Assured expectation that legitimately belongs inside the polling loop?Yes — the invariants that hold on every attempt. On a tram inspection status resource, `then().statusCode(200)` and a content-type check are true whether the inspection is `PENDING` or `SETTLED`, so keeping them inside makes a genuine transport or server failure blow up at once instead of reading as patience. Only the eventual value belongs outside.
saying these in an interview costs you the question
- Wraps the whole validating chain in catch (AssertionError) to poll
- Believes REST Assured returns false when an expectation fails
- Catches Exception and wonders why the loop still dies immediately
- Puts every expectation inside the loop, so the report names only the deadline
- Raises the deadline until the suite goes green again