General availability (GA) Grafana Cloud

Write resilient check scripts

A check should fail when the outcome you monitor fails. Fixed delays, selectors tied to page styling, and requests that continue after a failed prerequisite can make a check fail for a different reason or obscure the original failure.

Use these patterns when writing k6 browser checks and k6 scripted checks. The browser examples are fragments to use inside an asynchronous test function with an existing page. The HTTP examples use an existing response and a baseUrl for your application.

Wait for the expected content

A fixed delay assumes an action completes within a particular time. It waits unnecessarily when the application is fast and can read the page too early when the application is slower.

For example, this code waits five seconds regardless of whether the ratings list has finished updating:

JavaScript
await page.waitForTimeout(5000);
const listItem = await page.locator('li').textContent();

Instead, wait for the application data to finish loading before checking the expected content. QuickPizza displays No ratings yet while its ratings request is pending, so that text alone doesn’t prove the list is empty.

The following fragment uses an existing page and the URL of the ratings page, ratingsUrl. Import fail from k6 at the top of your script.

JavaScript
const ratingsPromise = page.waitForResponse(/\/api\/ratings$/);
await page.goto(ratingsUrl);
const response = await ratingsPromise;

if (response.status() !== 200 || (await response.json()).ratings.length !== 0) {
  fail('Expected an empty ratings list');
}

await page.waitForFunction(() => document.querySelector('li')?.textContent.trim() === 'No ratings yet');
const listItem = await page.locator('li').textContent();

Register the response wait before triggering the request. For actions that delete data and reload the page, also distinguish the deletion response from the refreshed data request. The browser check tutorial shows the complete sequence. Adapt the request URL, response validation, selector, and expected content to your application.

page.waitForFunction() returns when the condition becomes true or throws an error when its timeout expires. To wait for an element to become visible, hidden, attached, or detached, use locator.waitFor().

Use a timeout that leaves enough time for the rest of the journey within the check’s overall timeout. A timeout is an upper bound on waiting. It doesn’t require the check to pause for that entire duration. Avoid disabling timeouts to make a failing wait pass.

Avoid using networkidle as a readiness condition. Background requests can prevent it from occurring even when the application is ready. For details, refer to page.waitForLoadState().

Select elements by their purpose

A selector based on a styling class can stop matching after a visual change, even when the user journey still works:

JavaScript
await page.locator('button.w-32').click();

Prefer an element’s role and accessible name, or an attribute your application maintains for testing. For a button labeled Clear Ratings, use:

JavaScript
await page.getByRole('button', { name: 'Clear Ratings', exact: true }).click();

The selector describes the intended action and survives changes to the button’s width or color. Keep it specific enough to identify one element. If several buttons have the same name, scope the locator to the relevant part of the page rather than relying on their order.

For supported role and name options, refer to page.getByRole().

Validate a response before using its data

A later request often depends on a value returned by an earlier request. Reading that value without first validating the response can produce a misleading error. For example, this code assumes a create-rating response contains a valid ID:

JavaScript
const id = response.json('id');
http.del(`${baseUrl}/api/ratings/${id}`);

Validate the status, parse the body, and validate the required value before continuing. The following fragment assumes response is the result of a request to create a rating:

JavaScript
import http from 'k6/http';
import { check, fail } from 'k6';

// Inside the test function, after the create-rating request:
if (!check(response, { 'rating created': (r) => r.status === 201 })) {
  fail(`Create rating returned status ${response.status}`);
}

let id;
try {
  id = response.json()?.id;
} catch (error) {
  fail('Create rating returned invalid JSON');
}

if (!check(id, { 'rating has a valid ID': (value) => Number.isInteger(value) && value > 0 })) {
  fail('Create rating returned no valid ID');
}

const deletion = http.del(`${baseUrl}/api/ratings/${id}`);
if (!check(deletion, { 'rating deleted': (r) => r.status === 204 })) {
  fail(`Delete rating returned status ${deletion.status}`);
}

Place the imports at the top of your script and the remaining code inside your test function. Adapt the status codes and body validation to your API contract.

check() records the validation result and returns a boolean. A failed check doesn’t stop the script by itself. fail() throws an error and stops the current iteration, so dependent requests don’t run after the prerequisite fails.

When using the asynchronous check helper from k6-utils with browser code, await its result before deciding whether to continue, as shown in the browser check tutorial.

Preserve failure details and release browser resources

Give each validation a name that identifies the step and expected outcome, such as rating created or rating deleted. This helps you distinguish the first failure from errors in later steps.

Use try and finally to close a browser page even when a wait, action, or validation throws an error:

JavaScript
const page = await browser.newPage();
try {
  // Run the journey and validate its outcomes.
} finally {
  await page.close();
}

If you catch an error to add context, propagate the failure instead of logging it and continuing as though the step succeeded. For how to inspect results from individual probes, refer to Visualize check execution.