How do you use a Playwright timeout error's call log to find which actionability check never passed?
answer
- The error body is not a stack trace
- It replays the attempt line by line
- The last line reached names the check
- Interception prints the guilty node
- Attempt numbers prove it retried
basics
~20 sRead the call log printed under the error. It replays the attempt in order and stops at the condition that never held - element is not visible, not stable, not enabled, or a named node that intercepts pointer events.
solid answer
~50 sA Playwright action timeout is not a generic "element not found". The error prints a call log that replays the attempt: the locator being waited for, what it resolved to, `attempting click action`, `waiting for element to be visible, enabled and stable`, then either that they are, or the check that failed. The distinguishing lines are `element is not visible`, `element is not stable`, `element is not enabled`, and -- the most useful of them -- `<div class="toast">...</div> intercepts pointer events`, which names the node owning the click point. A `retrying click action, attempt #2` line proves the gate ran repeatedly rather than failing once. So: check the locator resolved, look at the markup it echoed, take the last check the log reached, and fix that condition in the app or retarget the locator. Changing the timeout comes last, if at all.
code
typescript · 8 linesimport { test } from '@playwright/test';
test('close button is not gated by a stale toast', async ({ page }) => {
await page.goto('/issues/412');
// A tight per-call budget prints the call log in seconds
// instead of burning the whole test timeout first.
await page.getByRole('button', { name: 'Close issue' }).click({ timeout: 5_000 });
});go deeper
Know that the text under a Playwright timeout is not noise: it lists what the runner waited for, and the last line is the condition that never became true.
Explain the shape of the log - waiting, resolved to, attempting, the check lines, retry attempts - and map each failure line back to the actionability check it belongs to.
Show the diagnostic order: resolution first, then the echoed markup, then the last check reached, then whether it retried. Fix that condition rather than widening the budget.
Own the habit across the team: an actionability timeout is triaged from its log and usually ends in an application fix, so failures keep pointing at product defects instead of being absorbed by suite settings.
## The error is a transcript, not a stack trace When a Playwright action never becomes actionable, the call rejects with a `TimeoutError` whose first line names the call and the budget -- `locator.click: Timeout 30000ms exceeded.` -- and whose body is a **call log**: an ordered replay of what the runner waited for, what the locator resolved to, and which condition stopped the attempt. Almost every actionability debugging session is won or lost on whether you read that block. ## Anatomy of a call log ``` TimeoutError: locator.click: Timeout 5000ms exceeded. Call log: - waiting for getByRole('button', { name: 'Close issue' }) - locator resolved to <button class="danger">Close issue</button> - attempting click action - waiting for element to be visible, enabled and stable - element is visible, enabled and stable - scrolling into view if needed - done scrolling - <div class="toast">Saved</div> intercepts pointer events - retrying click action, attempt #2 ``` Read it top down and three facts appear before you have looked at the app at all. The locator **did** resolve, so this is not a selector problem. The first three checks **did** pass. The point the click targeted belongs to a toast, and the runner has already started another attempt. ## The lines that name the failed check | Call-log line | Check | Typical cause | First move | |---|---|---|---| | `element is not visible` | Visible | not rendered yet, zero-size, or `display: none` | confirm the container rendered and the locator matched the intended node | | `element is not stable` | Stable | a transition or a reflow still running | wait on the settled state, or turn animations off in the test build | | `element is not enabled` | Enabled | a control gated on a pending request, or a disabled ancestor `fieldset` | drive the precondition the app is waiting for | | `<div ...> intercepts pointer events` | Receives events | a backdrop, banner, toast or sticky header over the point | dismiss the layer, or fix the layering defect it just reported | | `element is not editable` | Editable | a `readonly` field the app never released | make the app release it | ## Read it in this order 1. **Did the locator resolve?** If the log stops at `waiting for ...` with no `locator resolved to` line, nothing matched and the actionability checks never ran -- a different problem entirely. 2. **What did it resolve to?** The echoed markup often reveals the wrong node, or a `disabled` attribute you did not expect. 3. **Which check is the last one mentioned?** That is the condition that never became true. 4. **Did it retry?** `retrying click action, attempt #N` proves the gate ran repeatedly, so the condition was false for the whole window rather than momentarily. ## Common misreads - **Treating it as "element not found".** The log distinguishes the two, and the fixes are unrelated. - **Raising the timeout first.** A longer window only helps a condition that eventually turns true. When the log shows the same failed check on every attempt, a bigger budget just delays the same message. - **Reaching for `force: true` after an interception line.** Forcing skips the check, not the geometry: the toast still receives the click, and the failure moves to a later, vaguer assertion. - **Ignoring the named node.** The interception line tells you which element is in the way. That is usually a real defect a user would hit too. ## Make the failure arrive sooner A per-call `timeout` is a debugging instrument as much as a policy. In `@playwright/test` the action timeout is unbounded by default, so a stuck click burns the whole test budget before printing anything. Passing `timeout` on the suspect call, or setting `actionTimeout` while investigating, gets the same call log in a few seconds. Keep the number you shipped honest afterwards -- the point of the tighter budget is a faster diagnosis, not a permanently stricter suite. Finally, the log is not the only copy of this information. The same call and its failed check are recorded in the trace, so a failure that only happens on the runner can still be read line by line afterwards rather than reproduced by guesswork.
- The log shows the click was retried several times before timing out. What does that tell you?That the locator resolved and the gate ran repeatedly, so this is not a matching problem -- one condition stayed false for the entire window. Compare the last check named in each attempt: a condition that never flips points at application state, while one that flips late points at something slow that a tighter locator or a real readiness signal should express.
- What does an interception line give you that a plain timeout does not?It names the node that owns the click point -- a backdrop, a sticky header, a toast -- so you can go straight to the layer at fault rather than guessing. It is frequently a genuine z-index or dismissal defect that a user would hit as well, which makes it a bug report rather than a test problem.
saying these in an interview costs you the question
- Raises the timeout without reading the call log
- Treats every action timeout as element not found
- Says the log cannot show which check failed
- Adds force to silence an interception message
- Assumes a timeout means the locator matched nothing