TechEarl

Get and Set URL Query String Parameters in JavaScript (URLSearchParams)

Read, set, append, and delete query string parameters in JavaScript with URLSearchParams: get vs getAll, the no-leading-question-mark toString gotcha, updating the URL without a reload, and the object round-trip trap.

Ishan Karunaratne⏱️ 8 min readUpdated
Share thisCopied
Read, set, append, and delete URL query string parameters in JavaScript with URLSearchParams, then update the address bar with history.replaceState and no page reload.

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:

javascript
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:

javascript
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:

javascript
new URLSearchParams(window.location.search);   // "?q=mysql..."  leading ? stripped
new URLSearchParams("q=mysql&page=2");          // no ? also fine

But for anything beyond reading the current page's own query string, parse the whole URL and reach for its searchParams:

javascript
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.

javascript
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 ?:

javascript
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:

javascript
const qs = params.toString();
const href = qs ? `/search?${qs}` : "/search";   // add the ? only if non-empty

Note 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):

javascript
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 perf

params.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:

javascript
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.

javascript
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:

javascript
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:

javascript
// 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.

javascript
// Node, no import:
const params = new URLSearchParams("page=2&sort=date");
params.get("sort");   // "date"

FAQ

See also

Sources

Authoritative references this article was fact-checked against.

TagsURLSearchParamsquery stringJavaScriptURLhistory APIwindow.locationNode.js

Found this useful? Pass it on.

Copied

Ishan Karunaratne

Tech Architect · Software Engineer · AI/DevOps

Tech architect and software engineer with 20+ years building software, Linux systems, and DevOps infrastructure, and lately working AI into the stack. Currently Chief Technology Officer at a healthcare tech startup, which is where most of these field notes come from.

Keep reading

Related posts