Why does new URLSearchParams({ q: 'a b' }).toString() produce q=a+b while encodeURIComponent('a b') produces a%20b, and when does that difference cause a bug?
answer
- form encoding versus generic escaping
- one writes a plus, one writes %20
- the parser accepts both, the writers differ
- literal plus signs vanish into spaces
- path segments follow a different rule
basics
~20 sURLSearchParams serialises with the application/x-www-form-urlencoded rules, which encode a space as +, while encodeURIComponent uses generic percent-encoding and produces %20. Bugs appear when a value containing a literal + is concatenated into a query string by hand and read back as a space.
solid answer
~40 sThe two functions implement different encodings. `URLSearchParams.toString()` follows the `application/x-www-form-urlencoded` serialiser — the same format an HTML form POSTs — where a space becomes `+` and a literal `+` becomes `%2B`. `encodeURIComponent` implements the generic URI component escape, where a space becomes `%20` and it leaves `!'()*~-._` unescaped. Both round-trip correctly through `URLSearchParams` parsing, because the parser decodes `+` as a space *and* decodes `%20` as a space. The bug is in hand-built strings: if you write `'?q=' + value` with `value = 'C++'`, the parser hands back `'C '` — the pluses decode to spaces. Symmetrically, decoding a form-encoded value with `decodeURIComponent` leaves the `+` in place. The fix is to stop concatenating: let `URLSearchParams` do both the encoding and the decoding.
code
javascript · 12 linesconst url = new URL('/search', 'https://search.test/');
url.searchParams.set('q', 'C++ jobs');
url.searchParams.set('city', 'Sao Paulo');
console.log(url.href);
console.log(new URL(url.href).searchParams.get('q'));
// hand-built, unencoded: the plus signs decode as spaces
const broken = new URL('https://search.test/?q=' + 'C++ jobs');
console.log(JSON.stringify(broken.searchParams.get('q')));
console.log(encodeURIComponent('a b'));
console.log(new URLSearchParams({ x: 'a b' }).toString());go deeper
Know that spaces come out as + from URLSearchParams and as %20 from encodeURIComponent, and that you should pass raw, unencoded values to URLSearchParams.
Explain the two encodings by name — form-urlencoded versus generic percent-encoding — and show why a literal + in a hand-built query string comes back as a space.
Demonstrate that you debug these from the wire: read the actual request URL, spot double-encoded %25 or lost plus signs, and enforce one URL-building helper so raw values are only ever encoded once.
Own the contract end to end — agree with backend owners how multi-byte and plus-bearing values are encoded, since a mismatch between the browser's form-urlencoded output and a server's custom parser is a silent data-corruption class, not a cosmetic one.
## Two encodings that look like one A URL query string can be percent-encoded in two closely related but distinct ways, and the confusion between them is one of the oldest bugs on the web. **Generic percent-encoding** is what `encodeURIComponent` does. Every byte outside a small safe set is written as `%XX`. Space becomes `%20`. The unreserved characters `A-Z a-z 0-9 - _ . ! ~ * ' ( )` are left alone. **`application/x-www-form-urlencoded`** is what HTML forms have always used for `GET` query strings and `POST` bodies, and it is what `URLSearchParams.toString()` produces. It is percent-encoding plus one substitution: a space is written as `+`. Its literal-safe set is narrower — only `A-Z a-z 0-9 * - . _` survive unescaped — so `URLSearchParams` percent-encodes `!`, `'`, `(`, `)` and `~`, which `encodeURIComponent` does not. ```js new URLSearchParams({ q: 'a b' }).toString(); // 'q=a+b' encodeURIComponent('a b'); // 'a%20b' new URLSearchParams({ q: "o'neil (x)" }).toString(); // "q=o%27neil+%28x%29" encodeURIComponent("o'neil (x)"); // "o'neil%20(x)" ``` Neither is wrong. They are different serialisations of the same value, and a correct query-string *parser* accepts both: the form-urlencoded parser decodes `%20` to a space and `+` to a space. ## Where the asymmetry bites The encoders differ; the decoder does not. That mismatch means the two must never be mixed by hand. **Bug 1 — unencoded input concatenated into a query string.** The value contains a real plus sign, and the parser reads it as a space: ```js const value = 'C++'; const url = new URL('https://search.test/?q=' + value); // no encoding applied url.searchParams.get('q'); // 'C ' — both pluses became spaces ``` This destroys plus signs in phone numbers (`+15551234`), base64url-free base64 (which uses `+` and `/`), date offsets (`2026-01-01T00:00:00+01:00`), and search terms like `C++`. **Bug 2 — decoding a form-encoded value with the wrong decoder.** ```js decodeURIComponent('a+b'); // 'a+b' — the plus is NOT turned back into a space new URLSearchParams('q=a+b').get('q'); // 'a b' — correct ``` Code that pulls a raw substring out of `location.search` and runs `decodeURIComponent` on it will show users literal `+` characters where they typed spaces. **Bug 3 — encoding twice.** Running `encodeURIComponent` on a value and *then* passing it to `URLSearchParams.set` double-encodes it: the `%` of `%20` is itself escaped to `%25`, so the server receives `a%2520b` and the user sees `a%20b` on screen. `URLSearchParams` always encodes what you give it; hand it the raw value. ## The correct pattern Let the platform own both directions. Build with the object, read with the object, never touch the string in between: ```js const url = new URL('/search', 'https://search.test/'); url.searchParams.set('q', 'C++ jobs'); url.searchParams.set('city', 'São Paulo'); url.href; // 'https://search.test/search?q=C%2B%2B+jobs&city=S%C3%A3o+Paulo' new URL(url.href).searchParams.get('q'); // 'C++ jobs' — round-trips exactly ``` Note `C++` came out as `C%2B%2B`: the serialiser escaped the literal pluses precisely so the parser would not read them as spaces. Non-ASCII text is UTF-8 encoded then percent-escaped, which is why `ã` becomes `%C3%A3`. ## When you still need encodeURIComponent `URLSearchParams` covers the query string. It does **not** cover the other places a value can be interpolated into a URL, and there the `+`-for-space rule does not apply: ```js const id = 'a b/c'; const u = new URL(`/items/${encodeURIComponent(id)}`, 'https://api.test/'); u.pathname; // '/items/a%20b%2Fc' — %20, and the slash escaped so it is one segment ``` Inside a **path segment** a `+` is a literal plus, not a space, so form-urlencoding would be actively wrong there. The same is true for a fragment. So the rule of thumb is: `URLSearchParams` for anything after the `?`, `encodeURIComponent` for path segments and other components, and never both on the same value. ## Why interviewers ask Hand-rolled query building is a recurring source of production incidents — dropped plus signs, double-encoded percent signs, broken accented characters — and every one of them is invisible in the developer's own ASCII test data. Naming the two encodings, knowing the decoder accepts both while the encoders differ, and defaulting to the platform object shows you have debugged this rather than read about it.
- If a value is already percent-encoded, what happens when you pass it to URLSearchParams.set?It gets encoded again. `set('q', 'a%20b')` treats the `%` as data and escapes it to `%25`, serialising as `q=a%2520b`; the reader then sees the literal text `a%20b`. `URLSearchParams` always encodes its input, so pass raw values only — double encoding is the usual cause of `%25` appearing in a URL bar.
- Why does encodeURIComponent leave characters like ' and ( alone when URLSearchParams escapes them?They have different safe sets. `encodeURIComponent` implements the URI unreserved set plus a few legacy marks (`!'()*~`), while the form-urlencoded serialiser keeps only alphanumerics and `*-._`. Both outputs decode to the same value, so the difference is cosmetic for correctness — but it matters if something downstream compares query strings byte for byte.
- When should you reach for encodeURIComponent rather than URLSearchParams?When the value goes somewhere other than the query string — most often a path segment, as in `/items/${encodeURIComponent(id)}`. There a `+` means a literal plus, not a space, and a raw `/` would split the segment, so form-urlencoding would corrupt the value. Use `URLSearchParams` for everything after the `?`.
- How does the encoding handle non-ASCII characters such as an accented letter?Both encoders serialise the text as UTF-8 and percent-escape each byte, so `ã` becomes `%C3%A3` — two escapes for one character. The `URL` parser reverses it on read, so round-tripping is lossless. Length limits are therefore about bytes, not characters, which matters when a long non-Latin search term meets a server or CDN URL cap.
saying these in an interview costs you the question
- Calling the + a bug or an invalid character
- Running encodeURIComponent before URLSearchParams.set
- Using decodeURIComponent on a raw query substring
- Believing %20 and + are interchangeable everywhere
- Assuming a plus survives a hand-built query string