skip to content

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?

level: middleimportance: should knowfreq 45%

answer

  1. one object, bound for life
  2. changes flow both ways
  3. read-only accessor, no setter
  4. a fresh instance is a copy, not a handle
  5. copy the URL before you mutate it

basics

~20 s

It 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 lines
javascript
const 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=price

go deeper

for a junior

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().

for a middle

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.

for a senior

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.

for a principal

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

context