Two functions in JavaScript look almost identical and people reach for whichever one is autocompleted first: encodeURIComponent and encodeURI. They are not interchangeable, and picking the wrong one is a classic source of "the link works in dev but breaks in production" bugs. This guide sets out exactly what each encodes, where each belongs, what the other languages get wrong, and how to decode without crashing.
The precise difference
Both turn unsafe characters into %XX triplets. The only difference is how many characters they leave alone. encodeURIComponent encodes everything that is not in the unreserved set — including the characters that structure a URL. encodeURI deliberately spares the characters that make a URL a URL: / ? : # & = + @.
const full = 'https://example.com/p?q=a b&x=1';
encodeURI(full);
// "https://example.com/p?q=a%20b&x=1" (keeps : / ? & = )
encodeURIComponent(full);
// "https%3A%2F%2Fexample.com%2Fp%3Fq%3Da%20b%26x%3D1" (encodes all)
If you ever call encodeURIComponent on a complete URL, you have just destroyed that URL: the :// and the slashes are now %3A%2F%2F and the string is no longer something a browser will navigate to. There is exactly one rule that prevents most encoding bugs: encode values, not URLs.
Which one to use where
- Use
encodeURIComponentfor individual values: a query parameter, a path segment, a hash you build yourself. - Use
encodeURIonly when you already have a complete, valid URL and want to sanitise stray unsafe characters without touching its structure — for example, a URL that came from a config file where a space or a non-ASCII character may have slipped in. - Never use either to encode a whole URL you intend to fetch — encode the pieces and assemble.
A quick-reference cheat sheet
When in doubt, this is the whole decision:
| What you are encoding | Use | Space becomes |
|---|---|---|
| a single query value / path segment | encodeURIComponent | %20 |
| a whole already-valid URL (cleanup only) | encodeURI | %20 |
| a whole query string (many params) | URLSearchParams | + |
| a form request body | form encoding (see below) | + |
Building query strings the right way
Hand-concatenating '?q=' + encodeURIComponent(v) works, but the moment you have more than one parameter it gets fiddly and error-prone. The browser gives you URLSearchParams, which encodes every value correctly for you:
const p = new URLSearchParams();
p.set('q', 'a b');
p.set('page', '2');
const url = 'https://example.com/search?' + p.toString();
// "https://example.com/search?q=a+b&page=2"
Notice the space became +. That is the form-encoding convention again, and a server using standard form decoding will turn it back into a space. If you need strict %20 instead, build the string with encodeURIComponent yourself.
Sending a form body with fetch
The same convention applies when you post form data. Set the content type to application/x-www-form-urlencoded and let URLSearchParams do the encoding:
fetch('/api/search', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({ q: 'a b', page: '2' }),
});
// body sent: "q=a+b&page=2"
Do not hand-roll this body with encodeURIComponent and then also set the form content type — you will get %20 where the server's form decoder expects +, and a space in the value will arrive as the literal %20 on servers that are strict.
decodeURIComponent and its sibling decodeURI
Decoding has two functions that mirror the encoders. decodeURIComponent decodes a value; decodeURI decodes a whole URL and will refuse to decode % sequences that represent structural characters. In practice you almost always want decodeURIComponent for values pulled from searchParams.
const u = new URL('https://ex.com/s?q=a%20b');
u.searchParams.get('q'); // "a b" (already decoded by the URL parser)
decodeURIComponent('a%20b'); // "a b"
decodeURI('https://ex.com/a%20b'); // "https://ex.com/a b"
Why decodeURIComponent throws URIError
Decoding is not symmetric and safe. If the input contains a truncated percent sequence — a % not followed by two hex digits, or only one — decodeURIComponent throws a URIError. This happens with corrupted data, partial copy-paste, or strings that were never properly encoded.
decodeURIComponent('%E4%BD'); // URIError: malformed URI sequence
decodeURIComponent('%'); // URIError
The defensive pattern is a wrapper that falls back instead of crashing the request:
function safeDecode(s) {
try {
return decodeURIComponent(s);
} catch (e) {
return s; // return the raw string rather than throw
}
}
Returning the raw string is usually the right call: a value that failed to decode is at least visible, whereas an uncaught exception can take down the whole handler.
Other languages: the traps
- Python —
urllib.parse.quoteencodes spaces as%20;quote_plusencodes them as+and also turns slashes into%2F. Reaching forquote_pluswhen you meant to build a path silently corrupts the slashes. - Go —
url.QueryEscapeis named for queries and produces+for spaces, like form encoding. Do not feed its output into a path. - Java —
URLEncoder.encodeis built forapplication/x-www-form-urlencodedbodies. It turns spaces into+and encodes far more aggressively than you expect, including the slashes. Using it to build a path or a redirect target is a frequent mistake. - PHP —
urlencodeproduces+for spaces (form style);rawurlencodeproduces%20. Many PHP apps decode$_GETwith form rules, so a+arrives as a space — but if you built the URL on the JS side withencodeURIComponentyou sent%20, and PHP still decodes that to a space, so those two are usually compatible; the breakage shows up only with literal plus signs.
The double-encoding incident
A framework encodes your parameters for you. Your code also encodes them. The result: hello%20world becomes hello%2520world. The server decodes once, sees hello%20world as a literal value, and your search for "hello world" returns nothing. The fix is to encode at exactly one layer — pick the framework's automatic encoding or your manual encoding, not both.
// Express/Next automatically decodes req.query, so do NOT pre-encode client values:
// client: fetch('/api?q=' + encodeURIComponent(term)) // correct, single encoding
// server: req.query.q is already the decoded real string
A real scenario: a REST path with a user value
Path segments need encoding too, and the trap is that a slash in a value becomes a path separator. Encode each segment with encodeURIComponent:
function apiUrl(user) {
// WRONG: a username containing "/" would split the path
// return '/api/users/' + user;
// RIGHT:
return '/api/users/' + encodeURIComponent(user);
}
apiUrl('a/b'); // "/api/users/a%2Fb" -- one segment, not two
A habit that prevents most bugs: the round-trip check
When you are unsure whether your encoding is correct, do a round-trip: encode, then decode with the matching decoder, and compare to the original. If the decoded value equals what you started with, your encoding was right for that layer. If it differs, you encoded too much or too little. This single test catches both the double-encoding and the missing-encoding classes at once, and it takes ten seconds in a console.
const original = 'Tom & Jerry / 你好';
const enc = encodeURIComponent(original);
const dec = decodeURIComponent(enc);
console.log(dec === original); // true -- the round-trip is clean
One more pitfall: encoding the whole key=value at once
A frequent mistake is to build 'q=' + value and then, instead of encoding just the value, run encodeURIComponent over the entire 'q=' + value string. The = becomes %3D, so the server sees one parameter whose name is q%3D rather than a key called q with your value. Always encode the value, then concatenate the already-safe key: 'q=' + encodeURIComponent(value), never encodeURIComponent('q=' + value).
Takeaway
Encode values, not URLs. Use encodeURIComponent for the pieces and URLSearchParams for whole query strings and form bodies. Decode defensively with a try/catch wrapper. And when you switch languages, check whether that language's "encode" means %20 or + before you trust it.