encodeURI vs encodeURIComponent: Which One to Use
encodeURIComponent encodes 11 characters that encodeURI leaves alone, and those 11 characters decide whether a value stays inside its parameter.
JavaScript has two built-in functions for percent-encoding, and they differ in one respect: how much of the URL syntax they leave alone. encodeURI assumes it is given a complete URL and keeps the characters that give a URL its structure, such as /, ? and &. encodeURIComponent assumes it is given one piece of data and encodes those characters too, so the data cannot be mistaken for structure. Short version: to put a value into a URL, use encodeURIComponent, or better, the URL API.
The exact difference
Both functions convert every character they do not leave alone to UTF-8 and write each byte as % plus two uppercase hex digits, so é becomes %C3%A9 and € becomes %E2%82%AC. The only difference is the set of printable ASCII characters each one leaves alone:
| Characters | encodeURI | encodeURIComponent |
|---|---|---|
A-Z a-z 0-9 - _ . ! ~ * ' ( ) | Left alone | Left alone |
# $ & + , / : ; = ? @ | Left alone | Encoded |
Space and " % < > [ \ ] ^ ` { | } | Encoded | Encoded |
| Control characters and everything outside ASCII | Encoded as UTF-8 bytes | Encoded as UTF-8 bytes |
The middle row is the whole story. Those 11 characters separate the parts of a URL, so leaving them alone inside a value changes what the URL means:
const s = 'Tom & Jerry/2?';
encodeURI(s); // 'Tom%20&%20Jerry/2?'
encodeURIComponent(s); // 'Tom%20%26%20Jerry%2F2%3F'
When to use each
Use encodeURIComponent for anything you insert into a URL: a query parameter value or name, a single path segment, a fragment, or a whole URL passed as a parameter such as redirect=. Here is what goes wrong with the other function:
const q = 'rock & roll';
// Wrong: the & starts a new parameter
encodeURI('https://example.com/search?q=' + q);
// 'https://example.com/search?q=rock%20&%20roll'
// The server sees q = 'rock ' and a second parameter ' roll'
// Right
'https://example.com/search?q=' + encodeURIComponent(q);
// 'https://example.com/search?q=rock%20%26%20roll'
encodeURI is for the narrow case where you already have a complete, correctly structured URL that only contains spaces or non-ASCII characters, such as https://example.com/café?name=Zoë. Even then it has two traps:
- It encodes
%. A URL that is already partly encoded gets double-encoded:encodeURI('https://example.com/a%20b')returnshttps://example.com/a%2520b. - It encodes
[and]. These are used around IPv6 addresses, sohttp://[::1]:8080/becomeshttp://%5B::1%5D:8080/, which is no longer a valid host.
If you are assembling a URL from parts, new URL() is usually the better tool: it encodes each part according to where it appears.
const url = new URL('https://example.com/search');
url.searchParams.set('q', 'café & thé');
url.href; // 'https://example.com/search?q=caf%C3%A9+%26+th%C3%A9'
How they compare with RFC 3986
RFC 3986 defines two groups of characters. The unreserved characters (section 2.3) never need encoding: letters, digits and - . _ ~. The reserved characters (section 2.2) have a syntactic role: the general delimiters : / ? # [ ] @ and the sub-delimiters ! $ & ' ( ) * + , ; =.
Neither JavaScript function matches this exactly. encodeURIComponent leaves five sub-delimiters unencoded: ! ' ( ) *. Browsers and most servers do not care, but systems that compute signatures over the encoded URL do. OAuth 1.0 signatures (RFC 5849) and AWS Signature Version 4 both require every character except the unreserved ones to be encoded, so a value containing an apostrophe or parentheses can produce a signature mismatch. The usual fix:
function encodeRFC3986(str) {
return encodeURIComponent(str).replace(/[!'()*]/g,
c => '%' + c.charCodeAt(0).toString(16).toUpperCase());
}
encodeRFC3986("it's (fine)!*"); // 'it%27s%20%28fine%29%21%2A'
URLSearchParams and the plus sign
URLSearchParams, and therefore url.searchParams, does not use either function. It follows the application/x-www-form-urlencoded format that HTML forms submit, which differs in two ways: a space becomes +, and only letters, digits and * - . _ are left alone, so ! ' ( ) ~ are encoded too.
encodeURIComponent('a b+c'); // 'a%20b%2Bc'
new URLSearchParams({ q: 'a b+c' }).toString(); // 'q=a+b%2Bc'
Both results are correct in a query string. Trouble starts when the encoding and decoding sides disagree. decodeURIComponent does not turn + into a space, so decodeURIComponent('a+b%2Bc') returns a+b+c, while new URLSearchParams('q=a+b%2Bc').get('q') returns a b+c. Decode query strings with URLSearchParams, or replace + with a space before calling decodeURIComponent. In a path, + is just a plus sign.
decodeURI vs decodeURIComponent
The decoding functions mirror the encoding ones. decodeURIComponent decodes every escape sequence. decodeURI leaves alone the escapes for the characters encodeURI would not have encoded, # $ & + , / : ; = ? @, so that decoding a URL does not change its structure: decodeURI('%C3%A9%20%2F%3F') returns é %2F%3F, while decodeURIComponent returns é /?.
Both throw URIError: URI malformed (the V8 wording used by Chrome and Node.js) when a % is not followed by two hex digits, as in 100%, or when the bytes are not valid UTF-8, as in %E9 from a system that encoded é as Latin-1. The encoding functions throw the same error for a lone half of a surrogate pair, such as a string cut in the middle of an emoji.
escape() is not an alternative
escape() is a legacy function kept for old code. It leaves @ * _ + - . / alone, and it does not use UTF-8: characters up to U+00FF become a single Latin-1 byte and everything above becomes a non-standard %u sequence. escape('café €') returns caf%E9%20%u20AC, which servers expecting UTF-8 cannot decode, and decodeURIComponent('%E9') throws. Replace it with encodeURIComponent, and unescape() with decodeURIComponent, remembering that data already stored in the old format needs converting.
Double encoding
Every encoding function encodes % itself as %25, so running one twice produces sequences like %2520: encodeURIComponent(encodeURIComponent('a b')) returns a%2520b. Encode raw values exactly once, at the moment they go into the URL, and never encode a string you received already encoded. The one legitimate case is a URL inside another URL: encoding https://example.com/?q=a%20b as a parameter correctly gives ...q%3Da%2520b, and the receiving side decodes it once to get the inner URL back.
To compare the two modes on your own input, the URL Encoder has a selector for encodeURIComponent and encodeURI. URL Decode decodes as you type and treats + as a space, as form data does, so a value that contains a real plus sign should carry it as %2B.