URL Encoding: encodeURI vs encodeURIComponent
JavaScript has two URL encoding functions, and using the wrong one causes subtle bugs. Here's the difference, with examples and a decision tree.
JavaScript gives developers two functions for URL encoding: encodeURI and encodeURIComponent. They look similar, they sound similar, and they often produce identical output — but they encode different sets of characters, and using the wrong one silently breaks your URLs.
This is one of those JavaScript gotchas that everyone runs into eventually. Let's demystify it once and for all.
The 30-second version
encodeURIis for encoding a complete URL. It preserves characters that have structural meaning in URLs::,/,?,#,&,=,+,@,;,$,,.encodeURIComponentis for encoding a single component of a URL — a query parameter value, a path segment, a fragment identifier. It encodes everything exceptA-Z,a-z,0-9,-,_,.,~,!,',(,).
The distinction matters because URL syntax has two layers: the URL structure itself (which uses :, /, ? as delimiters) and the data inside the URL (which must not contain those delimiters unescaped, lest they be misinterpreted).
When to use which
Use encodeURIComponent for query parameter values
const query = 'name=John Doe&email=foo@bar.com';
const url = `https://example.com/search?q=${encodeURIComponent(query)}`;
// → https://example.com/search?q=name%3DJohn%20Doe%26email%3Dfoo%40bar.com
Notice that & and = inside the query string are encoded to %26 and %3D. This is correct — they're data, not URL structure. If you used encodeURI here, the & would survive unencoded, and the receiving server would interpret email=foo@bar.com as a separate query parameter.
Use encodeURI for complete URLs that contain non-ASCII characters
const url = 'https://example.com/path with spaces/файл.html';
const encoded = encodeURI(url);
// → https://example.com/path%20with%20spaces/%D1%84%D0%B0%D0%B9%D0%BB.html
The / and : are preserved because they're part of the URL structure. The spaces and Cyrillic characters are encoded because they're not valid in URLs.
Mix both for paths with dynamic segments
const baseUrl = 'https://example.com';
const segment = 'café résumé.pdf';
const fullUrl = `${baseUrl}/files/${encodeURIComponent(segment)}`;
// → https://example.com/files/caf%C3%A9%20r%C3%A9sum%C3%A9.pdf
Here you use encodeURIComponent on the segment because it's data inside the path — the / in café/résumé.pdf would otherwise be interpreted as a path separator.
Common mistakes
1. Using encodeURI for query parameter values.
// WRONG
const url = `?name=${encodeURI('foo&bar')}`;
// → ?name=foo&bar
// Server sees: name=foo, bar= (empty)
The & survives, and the server parses it as a parameter separator. Use encodeURIComponent instead.
2. Double-encoding.
// WRONG
const url = `?q=${encodeURIComponent(encodeURI('hello world'))}`;
// → ?q=hello%2520world
The first encoding produces hello%20world. The second encoding escapes the % to %25, producing hello%2520world. The server decodes once and gets hello%20world — not what you wanted.
3. Forgetting to encode UTM parameters.
UTM tags often contain spaces (e.g., utm_campaign=spring sale). If you don't encode them, the URL breaks at the first space. Worse, analytics tools may attribute traffic to the wrong campaign because the unencoded URL is split incorrectly.
4. Encoding already-encoded URLs.
If you pull a URL from the address bar and pass it through encodeURI again, the existing %20 becomes %2520. Always check whether the URL is already encoded before encoding it.
The unreserved set
RFC 3986 defines a small set of "unreserved characters" that have no special meaning and never need encoding:
- Letters:
A-Z,a-z - Digits:
0-9 - Hyphen:
- - Underscore:
_ - Period:
. - Tilde:
~
Everything else can technically appear in a URL, but most other characters have either structural meaning (:, /, ?) or must be encoded (spaces, non-ASCII, control characters).
encodeURIComponent encodes everything except the unreserved set plus a few "sub-delimiters" (!, ', (, ), *). This is more aggressive than strictly required by RFC 3986, but it produces URLs that work universally.
Encoding non-ASCII characters
Both encodeURI and encodeURIComponent encode non-ASCII characters as UTF-8 byte sequences, with each byte as %XX. A Cyrillic character like й becomes %D0%B9 (two bytes). An emoji like 🚀 becomes %F0%9F%9A%80 (four bytes).
This is correct behavior — modern browsers and servers expect UTF-8 in URLs. Older servers that use Latin-1 will mangle these characters, but that's their problem.
The full URL building pattern
For most URL construction tasks, the right approach is:
function buildUrl(baseUrl, params) {
const query = Object.entries(params)
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`)
.join('&');
return `${baseUrl}?${query}`;
}
buildUrl('https://example.com/search', {
q: 'café résumé',
lang: 'fr-FR',
page: 2,
});
// → https://example.com/search?q=caf%C3%A9%20r%C3%A9sum%C3%A9&lang=fr-FR&page=2
This handles edge cases like numeric values (via String(v)), empty parameters, and special characters consistently. Use URLSearchParams if you're targeting modern browsers — it handles encoding automatically.
What about URLSearchParams?
The modern URLSearchParams API (available in all modern browsers and Node 10+) handles encoding for you:
const params = new URLSearchParams({ q: 'café résumé', lang: 'fr-FR' });
const url = `https://example.com/search?${params.toString()}`;
// → https://example.com/search?q=caf%C3%A9+r%C3%A9sum%C3%A9&lang=fr-FR
Note: URLSearchParams uses + for spaces (form-encoding), not %20. Both are valid in query strings, but %20 is more universally correct in URLs.
The decision tree
- Are you encoding a complete URL? →
encodeURI - Are you encoding a single value inside a URL? →
encodeURIComponent - Building a URL from scratch? →
URLSearchParamsor a helper function
That's it. If you remember nothing else, remember that encodeURIComponent is the safer default — it over-encodes structural characters, but that's almost always better than under-encoding data characters.
TextOpal's URL Encoder tool handles both modes side-by-side, so you can see the difference instantly and copy the right one. Everything runs locally — your URLs and UTM parameters never leave your browser.