Is the object returned by a URL instance's searchParams property a live view of that URL or a detached snapshot, and what breaks in a helper that works on new URLSearchParams(url.search) instead?
answer
- one object, bound for life
- changes flow both ways
- read-only accessor, no setter
- a fresh instance is a copy, not a handle
- copy the URL before you mutate it
basics
~20 sIt is a live view: the same URLSearchParams object is returned every time, and mutating it rewrites the URL's search and href. A helper that builds new URLSearchParams(url.search) gets a detached copy, so its changes are silently lost unless it assigns the result back.
solid answer
~40 s`url.searchParams` is a read-only accessor that hands back one `URLSearchParams` object permanently bound to that `URL` instance. Mutations propagate both ways: calling `url.searchParams.set('page','2')` updates `url.search` and `url.href` immediately, and assigning `url.search = 'a=1'` re-populates the same params object. Because the property is read-only, `url.searchParams = something` does nothing in sloppy mode and throws in strict mode. By contrast, `new URLSearchParams(url.search)` copies the pairs into a fresh, unbound object — a perfectly good pattern, but the helper must finish with `url.search = params.toString()` or the edits vanish. The copy is also the right choice when you want to derive a candidate URL without mutating the original, since `URL` objects are mutable and shared references will see your changes.
code
javascript · 16 linesconst url = new URL('https://api.test/items?page=1');
const live = url.searchParams;
console.log(url.searchParams === live); // true
live.set('page', '2');
console.log(url.href); // ...?page=2
url.search = 'page=9&sort=name';
console.log(live.get('sort')); // 'name'
// detached copy needs an explicit write-back
const copy = new URLSearchParams(url.search);
copy.set('sort', 'price');
console.log(url.href); // still sort=name
url.search = copy.toString();
console.log(url.href); // now sort=pricego deeper
Remember that editing url.searchParams updates the URL itself, and that a URLSearchParams you construct yourself must be written back with url.search = params.toString().
Explain that searchParams is a read-only accessor returning one bound object, that assignment to it is a no-op or a TypeError, and that both directions of the binding stay in sync.
Show the reference-semantics discipline: URL objects are mutable and shared, so helpers should copy with new URL(url) and return a new instance rather than contaminate a shared base URL across requests.
Set the convention that URL manipulation goes through pure helpers with immutable inputs and a canonical parameter order, so link building stays deterministic across shared code, caching layers, and analytics comparisons.
## The property is an accessor, and it is live `URL.prototype.searchParams` is a getter with no setter. It returns the *same* `URLSearchParams` instance for the lifetime of the `URL` object, and that instance is wired to the URL's query component in both directions. ```js const url = new URL('https://api.test/items?page=1'); const p = url.searchParams; url.searchParams === p; // true — the same object every time p.set('page', '2'); url.search; // '?page=2' url.href; // 'https://api.test/items?page=2' url.search = 'page=9&sort=name'; p.get('sort'); // 'name' — the same object was re-populated ``` So there are two equivalent ways to edit the query on a `URL`: go through the params object, or assign a whole string to `url.search`. Both re-serialise `href`. Assigning `url.search = ''` clears the query and removes the `?` from `href`. Because the accessor has no setter, this fails: ```js 'use strict'; url.searchParams = new URLSearchParams('a=1'); // TypeError ``` Without strict mode the assignment is silently ignored, which produces the maddening "my code runs, nothing changes" bug. The correct spelling is `url.search = otherParams.toString()`. ## The detached copy `new URLSearchParams(url.search)` parses the query string into a brand-new object with no link back: ```js function addTracking(url) { const params = new URLSearchParams(url.search); // detached copy params.set('utm_source', 'newsletter'); return url; // BUG: url is unchanged } ``` The copy is not a mistake in itself — it is the right tool when you want to compute something without touching the original. It only becomes a bug when the author believes it is live. The fix is one line: ```js function addTracking(url) { const params = new URLSearchParams(url.search); params.set('utm_source', 'newsletter'); url.search = params.toString(); // write it back return url; } ``` Note that the constructor tolerates the leading `?` that `url.search` includes — it is stripped during parsing — while `toString()` emits no leading `?`, and the `search` setter accepts the string with or without one. ## Mutability is the deeper trap The live view is a symptom of something more important: **`URL` objects are mutable and passed by reference.** A helper that mutates the `URL` it was handed changes the caller's object too. ```js const base = new URL('https://api.test/items?page=1'); const tracked = addTracking(base); tracked === base; // true — the caller's URL was mutated ``` If `base` is a module-level constant reused for every request, that helper has permanently contaminated it, and each call appends more state. The disciplined pattern is to copy first and return a new object: ```js function withParam(url, name, value) { const next = new URL(url); // the URL constructor accepts a URL and stringifies it next.searchParams.set(name, value); return next; } ``` `new URL(url)` works because the constructor stringifies its argument via `href`, giving you a clean deep copy. Treating URL helpers as pure functions over immutable inputs removes an entire class of "why does this request carry last page's filters" bugs. ## Ordering and iteration while mutating One more consequence of liveness: `set` keeps a replaced key in its original position, while `append` and a set-on-a-missing-key add at the end. So repeatedly toggling a filter can reorder the query string even though the pairs are the same. If a stable string matters — comparing URLs, using one as a cache key, snapshot-testing a link — call `params.sort()` before serialising, which sorts pairs by name and preserves the relative order of values sharing a name. Also avoid mutating while iterating the live view. Deleting entries inside a `for...of` over the same params object can skip pairs, exactly as it would with any list you edit during traversal. Snapshot first with `[...params]` or `params.getAll(name)`, then mutate. ## Why interviewers ask This question separates people who have read the property from people who have debugged it. The read-only accessor, the silent no-op assignment in sloppy mode, and the detached-copy write-back are all real bugs that produce no error and no stack trace — the URL simply is not what you expected when the request goes out.
- What is the cleanest way to produce a modified copy of a URL without touching the original?`const next = new URL(url)` — the constructor stringifies the argument through `href`, so you get an independent object — then mutate `next.searchParams` and return it. Treating URL helpers as pure functions avoids the case where a shared base URL accumulates state across calls because an earlier helper mutated the caller's instance.
- What happens if you assign directly to url.searchParams?Nothing useful. `searchParams` is a getter with no setter, so in sloppy mode the assignment is silently discarded and in strict mode — which includes every ES module — it throws a `TypeError`. Assign the serialised string to the writable `search` property instead: `url.search = params.toString()`.
- Why might a query string come back in a different order after toggling a filter twice?`set()` on an existing name keeps that pair's original position, but a `set()` or `append()` for a name that is currently absent adds it at the end. Toggling a value off and on therefore moves it. Call `params.sort()` before serialising when the exact string matters — for comparing URLs, deduplicating requests, or snapshot tests.
saying these in an interview costs you the question
- Thinking searchParams returns a fresh copy each access
- Assigning to url.searchParams and expecting it to work
- Editing a copied params object without writing it back
- Assuming URL objects are immutable value types
- Mutating the live params while iterating over them