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.
| Method | Returns | When nothing matches |
|---|---|---|
querySelector(selector) | First matching Element | null |
querySelectorAll(selector) | Static NodeList of all matching elements | Empty 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()andlength. - The result can avoid array mutation methods like
push,pop, andsplice, 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
querySelectorreturns the first matchingElement, ornullif nothing matches.querySelectorAllreturns a staticNodeList, or an emptyNodeListif nothing matches.- A
NodeListis array-like, but it is not an array; convert it withArray.from()when you needmap,filter, orreduce. - 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.