skip to content

Testing Library exposes each query as getBy*, queryBy* and findBy* (plus the All variants). What does each return or throw, and which one do you use to assert that an element is NOT on the page?

level: middleimportance: must knowfreq 70%

answer

  1. prefix decides the failure mode
  2. only one variant returns null
  3. absence assertions need the null-returning one
  4. getBy also throws on multiple matches
  5. All variants differ on zero

basics

~20 s

getBy* returns the single match and throws if there is none (or more than one); queryBy* returns null instead of throwing; findBy* returns a promise that retries. Assert absence with queryBy*, because getBy* throws before your matcher can run.

solid answer

~40 s

The three prefixes differ only in failure behaviour. `getBy*` returns the matching element and throws a descriptive error — including a printed DOM — when nothing matches, which makes it the default for anything you expect to be present. `queryBy*` is identical except it returns `null` when nothing matches, so it is the one variant you can safely put inside a negative assertion such as `expect(screen.queryByRole('alert')).not.toBeInTheDocument()`. `findBy*` returns a promise for things that appear later. The `All` variants change arity: `getAllBy*` returns an array and still throws on zero matches, while `queryAllBy*` returns an empty array. The common bug is `expect(screen.getByText('Error')).not.toBeInTheDocument()` — that throws inside the query, so the assertion never runs and the failure message is confusing.

code

javascript · 18 lines
javascript
import { screen } from '@testing-library/dom'

document.body.innerHTML = `
  <ul>
    <li>Apples</li>
    <li>Pears</li>
  </ul>
`

// present: getBy gives a printed DOM on failure
screen.getByText('Apples')

// absent: queryBy returns null instead of throwing
console.log(screen.queryByText('Bananas')) // null

// counting: All variants differ on zero matches
console.log(screen.getAllByRole('listitem').length)   // 2
console.log(screen.queryAllByRole('alert').length)    // 0, no throw

go deeper

for a junior

Memorise the mapping: getBy throws, queryBy returns null, findBy returns a promise. Know that a not-in-the-document assertion must use queryBy.

for a middle

Explain both failure modes of getBy — zero matches and multiple matches — and why the throw is a feature: an ambiguous query is a test that could quietly assert on the wrong element.

for a senior

Argue for the failure message as a design concern: choosing getBy for presence gives the next engineer a printed DOM and the available roles when the test breaks at 2am, while queryBy gives them "received null".

for a principal

Frame it as suite-wide diagnosability — conventions about which variant goes where, and how a codebase that reaches for queryBy everywhere ends up with failures nobody can triage without rerunning locally.

## Two axes, not six queries Every Testing Library query name is built from two independent choices. The **prefix** decides what happens when the element is not there, and the **All** segment decides whether you get one element or a list. `getByRole`, `queryAllByText`, `findByLabelText` are all the same underlying matcher with different failure and arity behaviour. ## getBy* `getByRole('button', { name: 'Save' })` returns the single matching element. It throws if there are **zero** matches, and it also throws if there is **more than one** — an important detail people forget. The zero-match error prints the rendered DOM and, for role queries, the list of roles that were actually available, which is why `getBy*` is the right default: when it fails you learn why immediately. ```javascript const save = screen.getByRole('button', { name: 'Save' }) // throws "Unable to find an accessible element with the role \"button\" // and name \"Save\"" plus the printed DOM ``` Because it throws, `getBy*` doubles as an assertion. `screen.getByText('Saved')` on its own already fails the test if the text is missing — the `expect(...).toBeInTheDocument()` that usually follows is documentation more than logic. ## queryBy* `queryByRole` behaves identically on success but returns `null` on zero matches. It still throws on multiple matches. There is exactly one situation that needs it: asserting something is **absent**. ```javascript // correct expect(screen.queryByRole('alert')).not.toBeInTheDocument() // broken: getBy throws before expect() ever sees a value expect(screen.getByRole('alert')).not.toBeInTheDocument() ``` The broken version fails with "Unable to find an accessible element with the role alert" — technically the outcome you wanted, but reported as an error inside the query rather than a clean assertion failure, and it fails in exactly the case where the code is correct. That inversion is why it is a classic interview trap. Using `queryBy*` for a presence check is the opposite mistake: `expect(screen.queryByText('Saved')).toBeInTheDocument()` works, but when it fails you get "received value must be an HTMLElement... received null" instead of the printed DOM. Prefer `getBy*` whenever you expect the element to exist. ## findBy* `findBy*` returns a promise that resolves with the element once it appears, rejecting after a timeout. It exists for content that arrives after an await point. Two rules matter here: it must always be awaited, and there is no `findBy` equivalent for absence — you cannot "wait for nothing to be there" with a query alone. ## The All variants `getAllByRole('listitem')` returns an array of every match and throws when there are none. `queryAllByRole('listitem')` returns `[]` when there are none and never throws. So the natural way to assert on a count is: ```javascript expect(screen.getAllByRole('listitem')).toHaveLength(3) // list should exist expect(screen.queryAllByRole('alert')).toHaveLength(0) // list may be empty ``` A subtle consequence: `getAllBy*` is also how you legitimately handle several matching elements. If `getByRole('button', { name: /delete/i })` throws "found multiple elements", switching to `getAllBy*` and indexing is one option — usually a worse one than scoping the query, because an index encodes ordering the component is free to change. ## A decision rule you can say out loud - Expect it to be there now → `getBy*`. - Expect it to be absent → `queryBy*`. - Expect it to arrive later → `findBy*`, awaited. - Expect several → the `All` variant, with `getAllBy*` when at least one must exist and `queryAllBy*` when zero is a legal outcome. That rule covers essentially every query you will write, and it is what an interviewer is listening for. The follow-up is almost always "so what happens if two things match `getBy`?" — the answer is that it throws, deliberately, because an ambiguous query is a test that could silently start asserting on the wrong element.

  • Why does getBy* throw when two elements match, instead of returning the first one?
    Because silently returning the first match would let a test drift onto the wrong element after a UI change and still pass. Throwing forces you to disambiguate deliberately — by scoping the query to a region, or by making the accessible names distinct. It is a design choice that trades convenience for the test continuing to mean what you wrote.
  • If getBy* already throws when an element is missing, why do people still write expect(...).toBeInTheDocument()?
    Mostly readability: the assertion states the intent for the next reader and keeps the test's arrange-act-assert shape intact. It adds no real checking power for a presence case. The one place it is load-bearing is the negative form, where `queryBy*` returns null and the matcher is what turns that null into a pass or a failure.
  • How would you assert that a list rendered no items at all?
    Use the query-all variant: `expect(screen.queryAllByRole('listitem')).toHaveLength(0)`. `getAllByRole` would throw on zero matches, turning the expected outcome into an error. If the empty state renders a message instead, asserting on that message with `getByText` is usually the better test — it checks what the user actually sees.

saying these in an interview costs you the question

  • Uses getBy inside an expect(...).not.toBeInTheDocument() assertion
  • Thinks queryBy is the asynchronous variant
  • Believes getBy returns an array when several elements match
  • Wraps a query in try/catch to test that something is absent
  • Says queryAllBy throws when nothing matches

context