Knowledge Base/concepts/querySelector vs querySelectorAll: What Is the Difference?

querySelector vs querySelectorAll: What Is the Difference?

The difference between querySelector and querySelectorAll is not just "one element versus many elements." That is the visible part. The useful part is the contract each method gives you: querySelector gives you one Element or null; querySelectorAll gives you a static NodeList, even when that list is empty. Mix those contracts up and your DOM code starts collecting little TypeErrors in the corners.

Use querySelector when your next line expects one element. Use querySelectorAll when your next line expects a collection.

The short version

Both methods accept CSS selectors:

document.querySelector("#save-button");
document.querySelector(".card");
document.querySelector("[data-state='open']");

document.querySelectorAll("button");
document.querySelectorAll(".task[data-state='open']");
document.querySelectorAll("article.card");

The difference is what they return.

MethodReturnsWhen nothing matches
querySelector(selector)First matching Elementnull
querySelectorAll(selector)Static NodeList of all matching elementsEmpty NodeList

That "nothing matches" column matters more than beginners expect. One missing element can throw immediately. An empty collection is still safe to loop over.

querySelector returns the first match

querySelector searches from the element you call it on and returns the first matching element in document order.

const firstError = document.querySelector(".error");

if (firstError) {
  firstError.textContent = "Please check this field.";
}

If the selector does not match anything, the result is null:

const banner = document.querySelector("#promo-banner");

console.log(banner); // null, if no element has that ID

That is why you often see a guard before using the result:

const button = document.querySelector("#save-button");

if (!button) {
  throw new Error("Missing save button");
}

button.disabled = true;

The guard is not ceremony. It tells the rest of the function, "from here on, I really do have the element."

If you want the deeper debugging checklist for missing elements, read Why querySelector Returns Null and How to Fix It.

querySelectorAll returns every match

querySelectorAll returns all matching elements as a NodeList:

const tabs = document.querySelectorAll(".tab");

tabs.forEach((tab) => {
  tab.classList.remove("is-active");
});

If nothing matches, you do not get null. You get an empty NodeList:

const notices = document.querySelectorAll(".notice");

console.log(notices.length); // 0

That means this is safe:

document.querySelectorAll(".notice").forEach((notice) => {
  notice.remove();
});

If there are no notices, the callback simply never runs. No drama. Lovely, even.

The naive mistake

The most common bug is selecting one element when the page has several matching elements:

const items = document.querySelector(".todo-item");

items.classList.add("is-complete");

That only updates the first .todo-item. The rest are untouched because querySelector stops after the first match.

If the task is "update every matching element," use querySelectorAll and iterate:

const items = document.querySelectorAll(".todo-item");

items.forEach((item) => {
  item.classList.add("is-complete");
});

The opposite mistake is also common:

const heading = document.querySelectorAll("h1");

heading.textContent = "Dashboard";

That fails because heading is a NodeList, not an element. The list itself does not have textContent in the way you want. If you expect one heading, select one heading:

const heading = document.querySelector("h1");

if (heading) {
  heading.textContent = "Dashboard";
}

Why querySelectorAll returns a NodeList instead of an Array

querySelectorAll returns a NodeList because it is a DOM API, not an array API. The browser is handing you a collection of DOM nodes from the document tree. Historically, the DOM has its own collection types, including NodeList and HTMLCollection, because those objects represent nodes in a document, not ordinary application data.

That distinction gives the browser a tighter contract:

  • The result can expose DOM-focused behavior such as item() and length.
  • The result can avoid array mutation methods like push, pop, and splice, which would be misleading on a document query result.
  • The platform can preserve web compatibility with existing DOM collection behavior.

This is the bit that trips people up:

const items = document.querySelectorAll("li");

console.log(Array.isArray(items)); // false

A NodeList is array-like. It has indexes and length:

const items = document.querySelectorAll("li");

console.log(items[0]);
console.log(items.length);

Modern browsers also let you iterate it:

const items = document.querySelectorAll("li");

items.forEach((item) => {
  console.log(item.textContent);
});

for (const item of items) {
  console.log(item.textContent);
}

But it is still not an array. Array methods such as map, filter, and reduce are not available directly:

const items = document.querySelectorAll("li");

items.map((item) => item.textContent);
// TypeError: items.map is not a function

If you want array methods, convert the NodeList first:

const labels = Array.from(
  document.querySelectorAll("li"),
  (item) => item.textContent.trim(),
);

console.log(labels);

For the broader method tradeoffs, see JavaScript Array Methods and forEach vs map in JavaScript.

querySelectorAll returns a static snapshot

The NodeList returned by querySelectorAll is static. It contains the elements that matched at the moment you called it. If the DOM changes later, that existing list does not update itself.

const list = document.querySelector("#todos");
const items = document.querySelectorAll("#todos li");

console.log(items.length); // 2

const nextItem = document.createElement("li");
nextItem.textContent = "Review pull request";
list.append(nextItem);

console.log(items.length); // still 2
console.log(document.querySelectorAll("#todos li").length); // 3

That is usually a good thing. You get a stable snapshot for the work you are about to do. If you need the current DOM again after adding or removing elements, run the query again.

This is different from some older DOM collections, which can be live. That live behavior is one reason older examples using getElementsByClassName can feel odd. querySelectorAll is the calmer default.

Scope your selectors when the page repeats itself

Both methods can be called on document or on a specific element. Calling them on an element scopes the search to that element's descendants.

const card = document.querySelector("[data-product='keyboard']");

if (card) {
  const price = card.querySelector(".price");
  const actions = card.querySelectorAll("button");

  if (price) {
    price.textContent = "$89";
  }

  actions.forEach((button) => {
    button.disabled = false;
  });
}

That pattern matters in component-like markup. A global .price selector grabs the first price on the page. A scoped .price selector grabs the price inside the card you already chose. The browser is literal. It will not infer your design system for you, which is rude but consistent.

When to use each one

Use querySelector when:

  • The selector should match one meaningful element.
  • You want the first match only.
  • You need to read or update one element's properties.
  • You are prepared to handle null.

Use querySelectorAll when:

  • The selector can match several elements.
  • You need to loop over the matches.
  • You want a count with .length.
  • An empty result should simply mean "do nothing."

If you need early exit while looping through a collection, a for...of loop is often clearer than forEach. The JavaScript for...of Loop guide covers that control-flow shape.

Key Takeaways

  • querySelector returns the first matching Element, or null if nothing matches.
  • querySelectorAll returns a static NodeList, or an empty NodeList if nothing matches.
  • A NodeList is array-like, but it is not an array; convert it with Array.from() when you need map, filter, or reduce.
  • Use scoped selectors on an existing element when repeated markup makes global selectors too broad.
  • Choose the method by what your next line expects: one element or a collection.
Share this article:
javascriptdomfundamentals