A query string is ?tag=new&tag=sale. Using URLSearchParams, what does get('tag') return, and how do append() and set() differ when the key already exists?
answer
- a list of pairs, not a map
- first match versus every match
- one call grows, the other collapses
- null versus empty array on a miss
- delete is all-or-nothing by name
basics
~20 sURLSearchParams keeps every repeated key as its own entry: get('tag') returns only the first value, 'new', and getAll('tag') returns ['new','sale']. append() adds another entry, while set() collapses all existing entries for that key into one.
solid answer
~40 sA query string is an ordered list of name-value pairs, not a map, so duplicate names are legal and `URLSearchParams` preserves them. `get('tag')` returns the **first** matching value — `'new'` — and `null` if the key is absent; `getAll('tag')` returns every value as an array, `['new', 'sale']`, and an empty array when there is none. On the write side, `append('tag', 'x')` adds a third entry and leaves the existing two alone, so the string becomes `tag=new&tag=sale&tag=x`. `set('tag', 'x')` writes `'x'` into the first matching entry and deletes the rest, leaving just `tag=x`; if the key was absent it behaves like `append`. `delete('tag')` removes all entries with that name. The practical rule: `set` for a single-valued filter, `append` when building a multi-select.
code
javascript · 13 linesconst params = new URLSearchParams('?tag=new&tag=sale&sort=price');
console.log(params.get('tag')); // 'new'
console.log(params.getAll('tag')); // ['new', 'sale']
console.log(params.get('nope')); // null
const appended = new URLSearchParams(params);
appended.append('tag', 'clearance');
console.log(appended.toString());
const replaced = new URLSearchParams(params);
replaced.set('tag', 'clearance');
console.log(replaced.toString());go deeper
Remember get() gives you one value and getAll() gives you all of them, and that every value you read back is a string even if you set a number.
Explain the mechanics: the query is an ordered pair list, set() overwrites the first match and drops the rest while append() adds, and delete(name) clears them all.
Show the production angle — multi-select filters encoded as repeated keys, guarding against nullish values that serialise as 'undefined', and helpers that keep URL state idempotent so repeated clicks do not grow the query string.
Own the convention: decide once whether the app encodes multi-value state as repeated keys or as a delimited single value, since that choice binds the frontend, any shareable-link contract, and whatever server framework parses it.
## A query string is a list, not a map The biggest conceptual step here is that `?tag=new&tag=sale` is not malformed and is not "last one wins". The query component is an ordered sequence of name-value pairs, and repeated names are the standard way HTML forms encode a multi-select or a group of checkboxes with the same `name`. `URLSearchParams` models exactly that: an ordered list that can contain the same name many times. That is why the read API is split in two. ```js const params = new URLSearchParams('?tag=new&tag=sale&sort=price'); params.get('tag'); // 'new' — first match only params.getAll('tag'); // ['new', 'sale'] params.get('missing'); // null params.getAll('missing'); // [] params.has('tag'); // true ``` Note the asymmetry in the "absent" cases: `get` returns `null`, `getAll` returns an empty array. Code that does `params.get('page') || 1` therefore works, while `params.getAll('page').length` is the honest way to ask whether anything was supplied. Also note that a key present with an empty value — `?tag=` — makes `get('tag')` return the empty string `''`, not `null`, so `has()` is the correct existence check when an empty value is meaningful. ## Writing: append, set, delete ```js const p = new URLSearchParams('tag=new&tag=sale'); p.append('tag', 'clearance'); p.toString(); // 'tag=new&tag=sale&tag=clearance' const q = new URLSearchParams('tag=new&tag=sale'); q.set('tag', 'clearance'); q.toString(); // 'tag=clearance' const r = new URLSearchParams('tag=new&tag=sale&sort=price'); r.delete('tag'); r.toString(); // 'sort=price' ``` `append` always grows the list. `set` is specified as: if any pair with that name exists, set the value of the **first** such pair and remove all the others; otherwise append. That "first, then remove the rest" detail is what preserves position — `set` on `a=1&tag=x&b=2` keeps the new `tag` where the old one was rather than moving it to the end. `delete(name)` removes *every* pair with that name, which surprises people who expect it to drop one. ## Removing a single value from a repeated key Because `delete(name)` is all-or-nothing, dropping one value of a multi-select traditionally means rebuilding: ```js function removeValue(params, name, value) { const kept = params.getAll(name).filter((v) => v !== value); params.delete(name); for (const v of kept) params.append(name, v); return params; } ``` Browsers shipped a two-argument form, `delete(name, value)` and `has(name, value)`, in 2023-era releases, which removes or tests a specific pair. It is the cleaner call where you can rely on it; feature-detect or keep the rebuild helper if you must support older engines. ## Constructing and iterating The constructor accepts four shapes: a query string (a leading `?` is stripped for you), an array of `[name, value]` pairs, a plain object, or another `URLSearchParams`. Only the array and string forms can express duplicates — an object literal cannot have the same key twice, so `new URLSearchParams({ tag: 'new', tag: 'sale' })` is not even valid source code. ```js new URLSearchParams([['tag', 'new'], ['tag', 'sale']]).toString(); // 'tag=new&tag=sale' new URLSearchParams({ sort: 'price', page: '2' }).toString(); // 'sort=price&page=2' ``` Iteration yields one entry per pair, duplicates included, in insertion order: `for (const [name, value] of params)`, plus `entries()`, `keys()`, `values()` and `forEach()`. Values are always strings — a number you pass in is stringified, and `null`/`undefined` become the literal `'null'`/`'undefined'`, which is a classic source of `?page=undefined` in production. Guard before you set. Finally, `toString()` returns the serialised pairs **without** a leading `?`. Assigning it to a `URL`'s `search` is fine either way, since the setter tolerates the missing `?`, but concatenating it by hand needs one. ## What interviewers are checking They want to see that you do not treat a query string as an object. A candidate who reaches for `getAll` for a filter list, uses `set` for single-valued state and `append` for multi-valued state, and knows `delete` clears the whole name, will not write the classic bug where selecting a second filter silently replaces the first — or where the URL grows a fourth copy of `page` on every click.
- How do you distinguish a parameter that is absent from one that is present but empty, as in ?tag=?Use `has()`. With `?tag=`, `get('tag')` returns the empty string `''` and `has('tag')` returns `true`; when the key is absent entirely, `get` returns `null` and `has` returns `false`. Relying on truthiness collapses the two cases, which matters for flags like `?debug=` or a cleared search box.
- What happens if you pass a number or undefined as a URLSearchParams value?Everything is stringified. `set('page', 2)` stores `'2'`, and reading it back gives the string `'2'`, so numeric comparisons need `Number()`. More dangerously, `set('q', undefined)` stores the literal text `'undefined'` and serialises as `q=undefined` — the API has no notion of an omitted value, so guard and skip the call when the value is nullish.
- Which URLSearchParams constructor inputs can represent a repeated key?The string form (`'tag=new&tag=sale'`), an array of pairs (`[['tag','new'],['tag','sale']]`), and another `URLSearchParams` instance. A plain object cannot — a JavaScript object literal has one slot per key — so building from an object silently loses multi-value filters. Use the array form when duplicates are possible.
saying these in an interview costs you the question
- Saying duplicate keys are invalid or last-wins
- Expecting get() to return an array of values
- Using set() to add a second filter value
- Thinking delete(name) removes only one entry
- Assuming values come back as numbers, not strings