Common Errors and Fixes
The error messages Selenium, Playwright and the browser throw for bad XPath, what each one means, and the mistake that usually caused it.
Common Errors and Fixes
XPath failures come in two flavours: the expression is invalid (a syntax error, thrown immediately) or it is valid but wrong (matches zero or too many elements). This page covers both, starting with the messages you will see.
Syntax errors
”The string ’…’ is not a valid XPath expression”
Chrome’s message (SyntaxError: Failed to execute 'evaluate' on 'Document'). Selenium wraps it as InvalidSelectorException. Playwright reports Unexpected token or a similar parser message. Causes, in order of frequency:
| Cause | Example | Fix |
|---|---|---|
| Unbalanced quotes | //a[@href='x] | close the quote |
| Unbalanced brackets | //div[@id='a' | add ] |
| Quote inside quote | //a[text()='It's'] | "It's" or concat('It', "'", 's') |
| XPath 2.0 function | //a[ends-with(@href, '.pdf')] | see Functions Reference |
not without parentheses | //a[not @disabled] | not(@disabled) |
Missing @ | //a[href='x'] | @href (otherwise href is a child element) |
| Java escaping | "//a[@class=\"x\"]" malformed | mix quote types: "//a[@class='x']" |
| CSS habits | //a.active, //div#main | //a[contains(@class,'active')], //div[@id='main'] |
| Wrong axis name | //a/preceding-siblings::b | preceding-sibling (singular) |
| Double slash inside predicate misuse | //div[//span] (valid, but restarts at root) | //div[.//span] |
”Failed to execute ‘evaluate’: The result is not a node set”
You used an expression that returns a number, string or boolean (count(//tr), string(//h1)) where the framework needs elements. Use it in DevTools or executeScript, not as a locator.
InvalidSelectorException: “compound class names not permitted”
That one is a By.className error, not XPath, but it is often the reason someone switches to XPath. //*[contains(@class, 'btn') and contains(@class, 'primary')] is the XPath answer.
Valid but zero matches
The expression parses, and returns nothing. Work through these in order.
1. Case. //Button, //DIV, @Type will not match in HTML. Everything lowercase.
2. Whitespace. text()='Save' fails on "\n Save\n". Use normalize-space()='Save'. See Whitespace.
3. Text split across elements. //button[text()='Save'] fails on <button><span>Save</span></button>. Use normalize-space() or contains(., 'Save'). See text() versus ..
4. Namespaces. //svg, //path return nothing. Use //*[local-name()='svg']. See SVG and Namespaces.
5. iframe or shadow DOM. The element is in another document or tree. See Shadow DOM and iframes.
6. Timing. The element is not rendered yet. This is not an XPath problem; use an explicit wait (WebDriverWait, Playwright’s auto-wait, Cypress retry).
7. Attribute versus property. @value is the HTML attribute, which does not update as the user types. @checked reflects initial state. Use the framework’s property accessors for live state.
8. Hidden characters. Non-breaking spaces and zero-width characters in the text. Inspect with charCodeAt. See Whitespace.
9. Wrong document. The page navigated, the modal re-rendered, or the app is a different build. Re-inspect.
Valid but too many matches
The locator matches, but the test clicks the wrong one or the framework complains about ambiguity (Playwright’s strict mode: “locator resolved to 2 elements”).
1. Duplicated UI. Header and footer both have “Sign in”. Modal and page both have “Save”. Anchor to a landmark. See Anchoring to Stable Context.
2. Hidden clones. Responsive layouts render mobile and desktop versions of a nav; one is display:none. XPath cannot see visibility. Add [not(ancestor::*[@hidden])] or [not(ancestor::*[contains(@class,'mobile')])], or anchor to the visible container.
3. contains() too loose. contains(@class, 'btn') matches btn-secondary. Match the class token. contains(., 'Save') matches Saved. Use normalize-space()=.
4. Per-parent index. //li[1] returns the first li of every list. Use (//li)[1]. See Position and Indexing.
5. Predicate restarting at root. //div[//span[@class='x']] is true for every div if any such span exists anywhere. Use .//span.
StaleElementReferenceException
The element was found, then the page re-rendered it. The XPath is fine; the reference went stale. Re-find the element right before using it, or use a locator abstraction that re-resolves (Playwright locators do this automatically).
A debugging routine
- Paste the XPath into the DevTools Elements search. Read the count.
- If the count is 0, remove predicates from the right until something matches. The last predicate you removed is the problem.
- If the count is too high, add an anchor from the left.
- Run
$x(...)[0]and hover it to confirm it is the element you meant. - Only then put it in the test.
Try It Yourself
Open the XPath Validator →
Paste a broken locator and see whether the validator catches it before the browser does.
Next Steps
- Testing XPath in DevTools - The tools for step 1
- Performance and Efficiency - Slow locators and what makes them slow
- XPath Interview Questions - Many of these errors are interview favourites