To read a query string parameter in JavaScript, build a URLSearchParams object and call get. The whole thing is one line, no regex, no string splitting, no library:
const params = new URLSearchParams(window.location.search);
params.get("page"); // "2" (or null if absent)URLSearchParams is the built-in query string parser. It has shipped in every browser since around 2017 and it is also a global in Node, so the same code runs server-side without an import. The rest of this page is the full read/write surface: getAll for repeated keys, set vs append, deleting, iterating, pushing the change back into the address bar without a reload, and the two gotchas that trip people up.
Read a parameter: get, getAll, has
For a URL like https://techearl.com/search?q=mysql&tag=db&tag=perf&page=2:
const params = new URLSearchParams(window.location.search);
params.get("q"); // "mysql"
params.get("missing"); // null (not undefined, not "")
params.getAll("tag"); // ["db", "perf"] every value for a repeated key
params.has("page"); // true
params.has("page", "2") // true (second arg checks the value too)Two things worth pinning down. get returns null when the parameter is absent, so a falsy check (if (!params.get("x"))) also fires on an empty ?x= value; use params.has("x") when you need to distinguish "missing" from "present but empty." And get only ever returns the first value for a key. If a key can repeat (filters, tags, multi-select), you want getAll, which returns an array. That distinction is the single most common bug I see with this API, and it comes back below in the object round-trip.
Prefer new URL(...) over hand-passing location.search
You can construct URLSearchParams straight from window.location.search, and the constructor politely strips a leading ? if one is present, so both of these work:
new URLSearchParams(window.location.search); // "?q=mysql..." leading ? stripped
new URLSearchParams("q=mysql&page=2"); // no ? also fineBut for anything beyond reading the current page's own query string, parse the whole URL and reach for its searchParams:
const url = new URL(window.location.href);
url.searchParams.get("page"); // "2"new URL(...) gives you a live searchParams object wired to that URL. Mutate it (next section) and url.href reflects the change for free, which is exactly what you want when you are building a link rather than editing the current address. It also handles a full URL string, so you can parse a link you scraped or received, not just location.
Set, append, and delete (these mutate, and return undefined)
This is where the most-copied wrong snippet on the web lives, so be precise. set, append, and delete all mutate the object in place and return undefined. They do not return a new params object or a string. Do not write const next = params.set(...): next will be undefined.
const params = new URLSearchParams("tag=db&page=1");
params.set("page", "2"); // replaces: page is now "2". Returns undefined.
params.append("tag", "perf"); // adds another: tag is now ["db", "perf"]. Returns undefined.
params.delete("page"); // removes every "page". Returns undefined.The difference between set and append is the whole game: set replaces all existing values for the key with one, append adds an additional value and keeps the rest. Reach for set when a parameter is single-valued (a page number, a sort order); reach for append when it is a repeated key (another tag). delete with one argument removes every entry for that name; pass a second argument and it only removes entries matching that exact value.
toString() has NO leading question mark
When you serialize the params back to a string, toString() gives you the query without the leading ?:
const params = new URLSearchParams("a=1&b=2");
params.toString(); // "a=1&b=2" NOT "?a=1&b=2"This is the other classic mistake (and an error in a lot of older tutorials, which show a phantom ? in front). toString() is symmetrical with the constructor: the constructor strips a leading ?, toString() omits it. So when you stitch a full URL by hand, you add the ? yourself:
const qs = params.toString();
const href = qs ? `/search?${qs}` : "/search"; // add the ? only if non-emptyNote the guard: toString() returns an empty string when there are no params, and you do not want a dangling /search?. The asymmetry to remember is that URL.search and location.search do include the ? (they hand you "?a=1&b=2"), but URLSearchParams.toString() does not. Mixing those two up is what produces ??a=1 in the address bar.
Iterate: for...of over entries
URLSearchParams is iterable. Loop it with for...of over entries(), destructuring each [key, value] pair (repeated keys yield one entry each):
const params = new URLSearchParams("q=mysql&tag=db&tag=perf");
for (const [key, value] of params.entries()) {
console.log(key, value);
}
// q mysql
// tag db
// tag perfparams.keys() and params.values() exist too if you only need one side, and a plain for (const [k, v] of params) works because iterating the object defaults to entries().
Update the URL without a reload (history API)
Reading is half the job. The other half is reflecting state into the address bar so it is bookmarkable and shareable, without a full page navigation. That is what history.replaceState and history.pushState are for:
const url = new URL(window.location.href);
url.searchParams.set("page", "3");
// Replace the current entry (no new back-button step):
history.replaceState(null, "", url);
// Or push a new entry (adds a back-button step):
history.pushState(null, "", url);Use replaceState for state that should not pollute the back button (a filter the user is tweaking live, a scroll position); use pushState when each change is a distinct "place" the user should be able to navigate back to (a paginated view, a tab). Passing the URL object directly works because the history API accepts a URL or a string. Neither call triggers a reload or a network request, and neither fires a popstate event, so if you keep UI in sync with the query string, update that UI yourself right after the call.
Object to query string, and back (with the repeated-key trap)
Going from an object to a query string is direct: pass the object to the constructor.
const params = new URLSearchParams({ q: "mysql", page: "2" });
params.toString(); // "q=mysql&page=2"Going the other way, Object.fromEntries(params) looks like the obvious round-trip, and it works fine for single-valued keys:
const params = new URLSearchParams("q=mysql&page=2");
Object.fromEntries(params); // { q: "mysql", page: "2" }Here is the trap. Object.fromEntries collapses repeated keys and keeps only the last one, because a plain object cannot hold two values under the same key. A URL like ?tag=db&tag=perf becomes { tag: "perf" }, silently dropping db. (See JavaScript object methods: keys, values, entries, fromEntries for how fromEntries works under the hood.) When a key can repeat, do not flatten to an object: keep the URLSearchParams object, or build the array yourself with getAll:
// Safe shape for keys that can repeat:
const obj = {};
for (const key of new Set(params.keys())) {
obj[key] = params.getAll(key); // always an array
}
// { q: ["mysql"], tag: ["db", "perf"] }If you control the data and know every key is single-valued, Object.fromEntries(params) is fine and clean. If you do not, getAll is the only correct read. Once you have a plain array of values you can run the usual array methods over them.
It works in Node too
URLSearchParams (and URL) are globals in Node, no require or import. That matters when you are parsing a query string in a request handler or normalizing a URL in a script, and it means the parsing logic you write for the browser is identical on the server. If you are fetching with the query string you just built, the Fetch API guide covers attaching it to the request.
// Node, no import:
const params = new URLSearchParams("page=2&sort=date");
params.get("sort"); // "date"FAQ
See also
- JavaScript object methods: keys, values, entries, fromEntries: how the object round-trip in this article actually works, and why
fromEntriesdrops repeated keys. - Modern JavaScript array methods: once
getAllhands you an array of values, the map/filter/reduce toolkit you reach for next. - The JavaScript Fetch API guide: attach the query string you just built to a request.
Sources
Authoritative references this article was fact-checked against.
- URLSearchParams (official MDN reference)developer.mozilla.org
- URLSearchParams.toString() (official MDN reference)developer.mozilla.org
- URLSearchParams.delete() (official MDN reference)developer.mozilla.org
- URL.searchParams (official MDN reference)developer.mozilla.org
- History.replaceState() (official MDN reference)developer.mozilla.org





