---
title: Browser check troubleshooting: Playwright errors explained
description: Why a browser check failed, by the error text you see — strict mode violations, elements not found, navigation timeouts, text and URL mismatches, unreachable hosts, private addresses, crashes and out-of-memory — and how to fix each one.
canonical: https://watchfor.io/docs/monitors/browser-checks/troubleshooting
---

# Browser check troubleshooting: Playwright errors explained

Why a browser check failed, by the error text you see — strict mode violations, elements not found, navigation timeouts, text and URL mismatches, unreachable hosts, private addresses, crashes and out-of-memory — and how to fix each one.

Find the error text from the failed step below. When the dashboard recognises
an error, it shows a one-line hint under the failed step with a link to the
matching section here, and says whether the change usually belongs in the
**script** or on the **site**.

A quick way to tell the two apart: **if the check has never passed, suspect
the script. If it passed yesterday and nobody changed it, suspect the site.**
**Compare with last passing run** in the run details shows what changed, step
by step, with both screenshots.

## Strict mode violation: the selector matched several elements

```
Error: locator.click: Error: strict mode violation: getByRole('link') resolved to 47 elements
```

Playwright never guesses which element you meant: an action on a selector that
matches more than one element fails. **Usually the script.**

- Name it: `getByRole('button', { name: 'Sign in' })` instead of
  `getByRole('button')`.
- Scope it to a part of the page:
  `page.getByRole('navigation').getByRole('link', { name: 'Pricing' })`.
- If any of the matches will do, say so: `.first()`.
- Hidden duplicates count too — a sticky header or a mobile menu often repeats
  the same search box or link.

## Nothing matched the selector

```
TimeoutError: locator.fill: Timeout 15000ms exceeded.
Call log:
  - waiting for getByLabel('Email')
```
```
Error: expect(locator).toBeVisible() failed
Error: element(s) not found
```

Playwright waited (15 s for actions, 5 s for assertions) and nothing on the page
matched. **Script or site** — the history decides:

- **Never passed:** the selector is wrong. Label text, roles and names must
  match what the page renders — run `npx playwright codegen` and copy its
  selector, or open the trace and use its locator picker.
- **Used to pass:** the page changed (a relabelled button, a redesign), or it
  never got this far — look at the failure screenshot. Common causes:
  - a cookie banner or pop-up covers the page → handle it with
    [addLocatorHandler](/docs/monitors/browser-checks/recipes#cookie-banner);
  - the element is inside an iframe → [use the frame first](/docs/monitors/browser-checks/recipes#iframes);
  - an earlier step didn't do what it seemed to (a click that didn't submit) —
    add an assertion at the end of each step so the failure lands where the
    problem is.

## The page didn't finish loading

```
TimeoutError: page.goto: Timeout 30000ms exceeded.
Call log:
  - navigating to "https://example.com/login", waiting until "load"
```

Navigation waits for the page's `load` event, up to 30 s. **Usually the site:**
a page that takes that long, or never finishes, is a real finding. Check the
Web Vitals and failed requests of the run.

If the page is fine but a third-party script (chat, analytics, ads) keeps it
from ever reaching `load`, wait for less:

```ts
await page.goto(process.env.TARGET_URL!, { waitUntil: 'domcontentloaded' });
```

## The element shows different text

```
Error: expect(locator).toHaveText(expected) failed
Locator:  locator('h2')
Expected: "Welcome back"
Received: "Your password is invalid!"
```

The element is there, but its text is not what the script expects. **Script or
site:** an error message in its place (as above) means the site has a problem —
or a credential changed. If the copy changed on purpose, update the expected
text; a pattern such as `toHaveText(/welcome/i)` or `toContainText('Welcome')`
survives small edits.

## Ended up on a different page

```
Error: expect(page).toHaveURL(expected) failed
Expected pattern: /dashboard/
Received string:  "https://example.com/login"
```

The browser landed somewhere else. **Script or site.** Look at the failure
screenshot:

- back on the login page → the sign-in failed (wrong or expired credentials, a
  new CAPTCHA, a locked test account);
- a consent, locale or "choose your region" page → handle it as a
  [pop-up](/docs/monitors/browser-checks/recipes#cookie-banner);
- the right page at a new address → update the pattern.

## The host couldn't be reached

```
Error: page.goto: net::ERR_TUNNEL_CONNECTION_FAILED at https://shop.example.com/
```

Our browsers reach the internet through a guard that resolves every host
itself. This error means the name didn't resolve or the connection was refused
from our location. **Script or site:** check the spelling first. If the URL is
right, its public DNS is failing — or the name points at a private address,
which the next section covers.

## Address not reachable from our browsers

```
Error: page.goto: net::ERR_TUNNEL_CONNECTION_FAILED at https://10.0.0.12/
```

The run details list the host on the **Errors** tab under **Blocked by us**, with the
reason, for example *private address*. Browser checks reach the **public
internet only**: private networks (10/8, 172.16/12, 192.168/16), loopback,
link-local, cloud metadata addresses and IPv6 destinations are always refused,
for every customer's safety. **Point the check at a public URL**, such as a
public staging host protected by a login.

## The browser tab crashed

```
Error: page.waitForTimeout: Page crashed
```

The tab died mid-step. **Script or site:**

- a page that uses a lot of memory (huge images, runaway scripts) can crash the
  tab — try it in your own browser with the task manager open;
- a script that closes the page or context too early — look for a missing
  `await` before the step that failed, or a `page.close()` in the middle;
- "Target page, context or browser has been closed" after a crash is the same
  failure seen from the next action.

## Ran out of memory

```
The run used more than its 1536 MB of memory and was stopped.
```

Every run has a memory cap for the whole sandbox — browser and test together.
**Usually the script:** large arrays or buffers, many open pages or contexts,
big responses kept in variables. Close pages you open, keep only what you
assert on, and split a very long journey into two checks. The steps that
finished before the stop are still shown.

## The journey ran out of time

```
Error: page.waitForTimeout: Test timeout of 60000ms exceeded.
```

The whole test took longer than the check's timeout. If every step is simply
slow, raise the timeout in the check's **Settings** (plans set the maximum). If
one step hangs, its wait never came true — the step list shows which one, and
the sections above explain why. Avoid fixed `waitForTimeout` pauses: web-first
assertions (`await expect(...).toBeVisible()`) wait exactly as long as needed.

## It passes on my laptop but fails here

- Run it with the same runtime and config: [Run locally](/docs/monitors/browser-checks/run-locally)
  mirrors our settings (viewport 1280 × 720, `en-US`, UTC, timeouts).
- Our browsers come from data-centre locations. Sites behind bot protection,
  geo-blocking or an office IP allowlist may answer differently. Allow the
  user agent `WatchFor-BrowserCheck/1.0 (+https://watchfor.io/bot)` (before
  October 2026: `WatchForBot/1.0`) or the [/bot](/bot) addresses for your
  test account or a staging host.
- Variables: the **Variables** strip under the editor marks anything the
  script reads that isn't set — an unset variable is `undefined` in the run.
- Timezone and locale: dates and currencies render for `en-US` / UTC.

## Couldn't run (our side)

A grey **Couldn't run** means the run never started or never reported back,
because of a problem on our side. Your script wasn't the cause; it never opens
an incident and never lowers uptime. Try the test run again in a minute.

## Passed on retry (flaky)

A failed test is retried once inside the same run in a fresh browser. A pass on
the retry is **flaky**: reported, never an incident. Frequent flaky runs usually
point at a race in the script (a click before the page was ready, a fixed
timeout) or an intermittent problem on the site. Turn on the **Flaky journey**
alert to hear about a pattern.

---

Canonical page: https://watchfor.io/docs/monitors/browser-checks/troubleshooting · All docs: https://watchfor.io/docs · Site guide: https://watchfor.io/llms.txt
