encodeURI vs encodeURIComponent vs URLSearchParams: Which One Should You Actually Use in 2026?
Published on May 21, 2026 by The Kestrel Tools Team • 9 min read
A support ticket lands on a Tuesday: “User reports their email shows up wrong in the welcome email.” You pull the log. Where the address should be user+test@example.com, the backend captured user test@example.com — the + got eaten somewhere between the form and the database. You stare at the URL-building code. It uses encodeURIComponent. That’s the right one, isn’t it? Or was it encodeURI? Or URLSearchParams? You open three Stack Overflow tabs, two of them from 2014, and start guessing.
This is the encodeURI vs encodeURIComponent vs URLSearchParams problem in JavaScript, and it’s been quietly shipping bugs for a decade. The three APIs look interchangeable. They are not. They produce different output for the same input, by design, and picking the wrong one corrupts query strings, fragments, and form data in ways that pass code review and only surface in production logs.
The short answer: in 2026, you almost always want URLSearchParams for query strings, encodeURIComponent for individual values you’re stitching into a URL by hand, and encodeURI essentially never. The rest of this post shows the same input running through all three APIs, explains why their outputs differ, and walks through the corner cases (the + vs %20 legacy, fragment encoding, Unicode) that the older blog posts skip.
A direct answer: which URL encoder should I use in JavaScript?
Use URLSearchParams when you’re building a query string. Use encodeURIComponent when you’re encoding a single value to drop into a URL path or hand-built query. Use encodeURI only when you have an entire URL that’s already syntactically valid and you just want to escape spaces and non-ASCII characters in it. That’s it. If you’re not sure, default to URLSearchParams — it’s the only one of the three that was designed for query-string building, and the only one that handles repeated keys, deletion, and parsing in addition to encoding.
The rest of the confusion comes from one historical detail: URLSearchParams encodes the space character as + (the application/x-www-form-urlencoded legacy from HTML forms), while encodeURIComponent encodes it as %20. Both decode back to a space on a correctly written server. The reason this causes bugs is that not every server is correctly written.
The same input, three different outputs
Here’s the canonical demonstration. Take the string hello world&filter=a+b — a value containing a space, an ampersand, an equals sign, and a literal +. Run it through each API:
const input = 'hello world&filter=a+b';
encodeURI(input);
// 'hello%20world&filter=a+b'
encodeURIComponent(input);
// 'hello%20world%26filter%3Da%2Bb'
new URLSearchParams({ q: input }).toString();
// 'q=hello+world%26filter%3Da%2Bb'
Three outputs, three different strings, one input. Read the differences carefully:
encodeURIonly escaped the space (%20). It left&,=, and+alone, because those are reserved characters with structural meaning in a URL.encodeURIassumes you handed it a complete, structurally valid URL and that the reserved characters are doing their job.encodeURIComponentescaped everything that wasn’t an unreserved character: space became%20,&became%26,=became%3D,+became%2B. It assumes you handed it a single value to embed inside a URL component, where reserved characters need to be neutralized.URLSearchParamsescaped the same reserved characters asencodeURIComponent, but encoded the space as+instead of%20. It also wrapped the result inq=...because that’s its job: build a query string, not encode a value.
If you stick that URLSearchParams output into a URL and a backend reads the query with the standard application/x-www-form-urlencoded rules (which is what every mainstream framework does by default — Express, Django, Rails, ASP.NET, Spring), the + decodes back to a space and you get hello world&filter=a+b on the other end. If you stick the encodeURIComponent output into a query string and a backend reads it the same way, the %20 also decodes back to a space and you also get the right answer. Both work. The bugs come from mixing them.
What encodeURI actually does (and why you almost never want it)
encodeURI was introduced in ECMAScript 3 (1999) for one specific job: take a complete URL string and make it safe to use in contexts that don’t allow spaces or non-ASCII characters — like the href attribute of an HTML link or a Location header. It escapes the unsafe characters but leaves alone the characters that have structural meaning in a URL.
The characters encodeURI does not escape are: A-Z a-z 0-9 - _ . ! ~ * ' ( ) ; , / ? : @ & = + $ #. That set includes every URL delimiter (/, ?, &, =, #) plus some characters that used to be reserved in older RFCs (!, *, (, )).
encodeURI('https://example.com/path with spaces?q=a&b=1#frag');
// 'https://example.com/path%20with%20spaces?q=a&b=1#frag'
That output is correct: the URL structure is preserved, and the only thing escaped is the literal space in the path. This is the use case encodeURI was built for.
The problem is that this use case rarely shows up in modern code. If you’re building URLs, you’re almost always assembling them from parts — a base URL plus a path segment plus query parameters — and each of those parts is a value, not a structurally-valid URL fragment. encodeURI is the wrong tool for encoding a value, because it leaves reserved characters alone. Drop a user-supplied search term into a URL with encodeURI and the first user who searches for cats & dogs breaks your query parsing.
In 2026, the cases where encodeURI is the right answer are roughly: (a) you have a complete URL string from somewhere else and you need to escape any spaces or non-ASCII characters in it before putting it in an href, and (b) you’re writing a polyfill or a URL-rewriting middleware that needs the exact ECMAScript 3 escaping behavior. For everything else, reach for encodeURIComponent or URLSearchParams.
What encodeURIComponent does (the right tool for individual values)
encodeURIComponent is the workhorse. Its job is to take a single string and percent-encode every character that isn’t an unreserved character per RFC 3986. The unreserved set is just A-Z a-z 0-9 - _ . ! ~ * ' ( ). Everything else — including all the URL delimiters — gets escaped.
encodeURIComponent('cats & dogs');
// 'cats%20%26%20dogs'
encodeURIComponent('user+test@example.com');
// 'user%2Btest%40example.com'
encodeURIComponent('café');
// 'caf%C3%A9'
That last example is worth pausing on: café in UTF-8 is the bytes c3 a9 for the é, and encodeURIComponent escapes them as %C3%A9. Both encodeURI and encodeURIComponent use UTF-8 byte encoding for non-ASCII characters — there’s no latin1 mode and there hasn’t been since ES3. If your server isn’t decoding the query string as UTF-8, the bug is on the server side.
The canonical mistake encodeURIComponent prevents is the email-with-plus-sign bug. user+test@example.com is a perfectly valid email address (the + is the gmail tag separator). If you stick that into a URL with no encoding and a server parses it with form-urlencoded rules, the + decodes back to a space and your welcome email goes to user test@example.com, which doesn’t exist. encodeURIComponent escapes the + to %2B, the server decodes %2B back to +, and the email is intact.
Use encodeURIComponent when:
- You’re hand-building a URL string with template literals or
+. - You’re encoding a path segment (e.g.
/users/${encodeURIComponent(username)}/profile). - You’re encoding a single query value and you don’t want a
URLSearchParamsobject for some reason. - You’re encoding a fragment value (the part after
#).
The one thing encodeURIComponent doesn’t do is parse URLs. It only encodes. If you need to read query parameters back out, you need URLSearchParams or the URL object — decodeURIComponent alone doesn’t split on & or handle repeated keys.
What URLSearchParams does (and the + vs %20 legacy)
URLSearchParams is the newest of the three. It’s been Baseline since 2020, which means every modern browser and every supported version of Node.js (16+) ships it natively. It’s the only one of the three that’s a real query-string API rather than a string-encoding function: it handles parsing, building, repeated keys, ordering, deletion, and iteration in addition to encoding.
const params = new URLSearchParams();
params.set('q', 'hello world');
params.set('filter', 'a+b');
params.append('tag', 'red');
params.append('tag', 'blue');
params.toString();
// 'q=hello+world&filter=a%2Bb&tag=red&tag=blue'
Three things to notice in that output:
- The space in
'hello world'became+, not%20. - The literal
+in'a+b'became%2B, exactly the same asencodeURIComponentwould have done. - The repeated
tagkeys both made it through, in insertion order.
The + for space is the application/x-www-form-urlencoded encoding from HTML forms, baked into the WHATWG URL spec because it’s what every browser actually sends when an HTML form is submitted with method="GET". The spec authors had a choice between matching forms (use +) and matching encodeURIComponent (use %20), and they chose forms. Every form-urlencoded parser ever written, on every server framework, decodes both + and %20 to a space when it’s reading a query string. So in practice, URLSearchParams output is interoperable with every backend.
The corner case: if you’re building a URL and you want to dump the URLSearchParams output into a non-query context (a path, a fragment, a non-form-urlencoded body), the + is wrong, because in those contexts + is just a literal +. Don’t do that. Use URLSearchParams for query strings only.
For query strings in 2026, URLSearchParams is the right default for four reasons:
- It’s a real API, not a stringly-typed function. You can’t accidentally double-encode a value, because the API takes raw values and encodes them once.
- It handles repeated keys (
?tag=red&tag=blue) correctly. Hand-built query strings withencodeURIComponentneed careful work to do the same. - It works symmetrically with the
URLobject.new URL(href).searchParamsgives you aURLSearchParamsyou can mutate and re-serialize. - It parses too.
new URLSearchParams(window.location.search).get('q')is the canonical way to read a query parameter, and it handles+,%20, and Unicode correctly.
A side-by-side decision table
| Situation | Use |
|---|---|
| Building a query string from values | URLSearchParams |
| Reading a query parameter from a URL | URLSearchParams (via new URL(href).searchParams) |
| Encoding a single path segment | encodeURIComponent |
Encoding a single fragment value (after #) | encodeURIComponent |
| Stitching a URL by hand with template literals | encodeURIComponent per value |
Escaping a complete, already-valid URL for an href | encodeURI |
| Encoding a value to put in a non-query context (e.g. path) | encodeURIComponent, not URLSearchParams |
The single rule that covers the most common case: if you see yourself writing ?q=${something}&filter=${something_else} in a template literal, stop and write new URLSearchParams({ q: something, filter: something_else }).toString() instead. Same output, no encoding bugs.
The fragment-encoding gotcha
encodeURI does not escape #, because # is a structural delimiter in a URL (it separates the path/query from the fragment). That means encodeURI is unsafe for any value that might contain a literal #:
encodeURI('search#term');
// 'search#term' — the # is preserved as a fragment delimiter
encodeURIComponent('search#term');
// 'search%23term'
If you’re encoding a value that gets concatenated into a URL, and the value can contain #, you must use encodeURIComponent. This is the most common reason encodeURI produces a subtly broken URL: the encoder doesn’t know whether your # is structural or accidental, and it defaults to assuming structural.
The symmetric concern: if you’re decoding, decodeURIComponent is the inverse of encodeURIComponent, and decodeURI is the inverse of encodeURI. Mixing them produces silent corruption. Always pair the encode and decode functions: encodeURIComponent with decodeURIComponent, encodeURI with decodeURI. URLSearchParams decoding happens automatically when you read values out of the object — you never call decodeURIComponent yourself.
Unicode and the ES3 quirk
All three APIs handle Unicode the same way: characters outside ASCII get UTF-8 byte-encoded and each byte gets percent-escaped. ä¸ć–‡ becomes %E4%B8%AD%E6%96%87. There’s no latin1 mode, and there hasn’t been one since ES3. If you’re seeing mangled non-ASCII characters, the bug is somewhere in the decoding chain, not in the encoder.
The one ES3 quirk worth knowing: encodeURI and encodeURIComponent both throw a URIError on lone surrogates (malformed UTF-16 strings). URLSearchParams does not — it silently replaces lone surrogates with U+FFFD (the replacement character) before encoding. If you’re encoding strings that might contain malformed Unicode (user input from older systems, sometimes), URLSearchParams is more forgiving. encodeURIComponent will crash, which is sometimes what you want and sometimes not.
A working example
Putting it all together: here’s how to safely build a URL in 2026.
// Building a URL with query parameters — use URLSearchParams
const url = new URL('https://api.example.com/search');
url.searchParams.set('q', 'hello world');
url.searchParams.set('email', 'user+test@example.com');
url.searchParams.append('tag', 'red');
url.searchParams.append('tag', 'blue');
url.toString();
// 'https://api.example.com/search?q=hello+world&email=user%2Btest%40example.com&tag=red&tag=blue'
// Encoding a single path segment — use encodeURIComponent
const username = 'alice/bob';
const profileUrl = `https://example.com/users/${encodeURIComponent(username)}/profile`;
// 'https://example.com/users/alice%2Fbob/profile'
// Reading a query parameter back out — use URLSearchParams
const incoming = new URL('https://example.com/search?q=hello+world&email=user%2Btest%40example.com');
incoming.searchParams.get('q'); // 'hello world'
incoming.searchParams.get('email'); // 'user+test@example.com'
No manual encodeURIComponent, no template-literal concatenation, no + vs %20 ambiguity. The URL object and its searchParams handle every encoding decision correctly.
If you want to test the same input through all three APIs and see the differences in your browser, try the Kestrel Tools URL Encoder/Decoder — paste a string, see the output of encodeURI, encodeURIComponent, and URLSearchParams side by side, and decode any percent-encoded string back to its original form. It runs entirely client-side, so the URLs you’re testing (often containing API keys, tokens, or PII) never leave your machine.
The takeaway
The three APIs are not interchangeable, and the differences matter:
encodeURIescapes only the unsafe characters in a complete URL. It’s almost never what you want in 2026.encodeURIComponentescapes every reserved character. It’s the right answer for encoding individual values when you’re stitching a URL by hand.URLSearchParamsis a real query-string API. It encodes spaces as+(because that’s what HTML forms do), handles repeated keys, and parses as well as builds. It’s the right default for query strings.
The single most common bug in this space is using encodeURI for a value that contains &, =, +, or # and watching the URL silently corrupt. The single most common cause of confusion is URLSearchParams’s + for space — it looks wrong, but it’s the spec-compliant application/x-www-form-urlencoded encoding and every server framework decodes it correctly.
Next time you reach for one of these, run the input through Kestrel Tools URL Encoder first and compare the three outputs. If they’re all the same, any of the three works. If they differ, you now know which one your code actually needs.