URL encoding: encodeURI vs encodeURIComponent, + vs %20

Percent-encoding with real outputs: what each JavaScript function escapes, when a space is + and when it is %20, and the bugs that double-encode.

On this page

A link breaks, a search for C++ returns results for C, a Vietnamese file name turns into a wall of %E1%BB. All three are URL encoding problems, and all three come from using the right idea in the wrong place. This article shows what percent-encoding does to real strings, the three encodings the web actually uses, and the mistakes that produce those symptoms.

Every output below was produced by our URL encoder, decoder and parser, which offers all three encodings side by side and takes a URL apart into its pieces.

What percent-encoding is

A URL may only contain a small set of ASCII characters, and some of them have jobs: / separates path segments, ? starts the query, & separates parameters, = separates a key from its value, # starts the fragment. Any character that is not allowed, or that would be mistaken for one of those separators, is written as its UTF-8 bytes, each as % and two hex digits.

So é is the two bytes C3 A9 and becomes %C3%A9. The Vietnamese ở is three bytes and becomes %E1%BB%9F. The emoji 🍜 is four bytes, %F0%9F%8D%9C. A literal percent sign must itself be encoded, as %25, or the decoder will try to read the next two characters as hex.

The three encodings

The web uses three slightly different rules, and choosing between them is most of the work.

`encodeURIComponent` is for one piece of a URL: a single path segment, a single query key or value. It escapes everything except letters, digits and - _ . ! ~ * ' ( ). Separators like / ? & = # are all escaped, because inside a value they are just characters.

`encodeURI` is for a whole URL that is already put together. It leaves the separators ; , / ? : @ & = + $ # alone, so the structure survives, and only escapes what is not allowed at all, such as spaces and non-ASCII letters.

Form encoding (application/x-www-form-urlencoded) is what HTML forms and URLSearchParams produce. It writes a space as +, and it escapes a few characters that encodeURIComponent leaves alone: ~ ! ' ( ).

The same inputs through all three:

InputencodeURIComponentencodeURIform
Phở & bún chảPh%E1%BB%9F%20%26%20b%C3%BAn%20ch%E1%BA%A3Ph%E1%BB%9F%20&%20b%C3%BAn%20ch%E1%BA%A3Ph%E1%BB%9F+%26+b%C3%BAn+ch%E1%BA%A3
a+b=ca%2Bb%3Dca+b=ca%2Bb%3Dc
50% off50%25%20off50%25%20off50%25+off
it's (fine)!~it's%20(fine)!~it's%20(fine)!~it%27s+%28fine%29%21%7E

Look at the first row. encodeURI left the & alone. If you used it to put Phở & bún chả into a query value, the server would see the value end at Phở and a second parameter, named bún chả and with no value, start after the &.

And a whole URL through each:

input:     https://example.com/tìm kiếm?q=phở bò&x=1#top
encodeURI: https://example.com/t%C3%ACm%20ki%E1%BA%BFm?q=ph%E1%BB%9F%20b%C3%B2&x=1#top
component: https%3A%2F%2Fexample.com%2Ft%C3%ACm%20ki%E1%BA%BFm%3Fq%3Dph%E1%BB%9F%20b%C3%B2%26x%3D1%23top

encodeURI gives a working link. encodeURIComponent gives a string that is no longer a link at all, but is exactly what you want if this whole URL is itself the value of a parameter, for example a redirect= or a share link.

The practical rule: build URLs from parts, and encode each part with encodeURIComponent (or let URLSearchParams do the query). Use encodeURI only to tidy a complete URL someone typed.

Space: + or %20?

Both are correct, in different places. In the query of a form-encoded URL, + means a space. In the path, + is a literal plus sign and a space must be %20. The confusion runs the other way too: when decoding, a form decoder turns + into a space, while decodeURIComponent leaves it as +. The tool shows the difference on a+b%20c: the component decoder gives a+b c, the form decoder gives a b c.

This is the C++ bug. A search box that puts the raw text into the query produces ?q=c++. A server reading it as a form decodes both plus signs to spaces and searches for c followed by two spaces. Encoded properly, the query is q=c%2B%2B, and the parser reads it back as c++.

Our tool's query editor notices which style a URL already uses and keeps it, so editing one parameter does not silently switch every space from + to %20.

Decoding errors worth understanding

  • A stray `%`. 100% cannot be decoded strictly, because % must be followed by two hex digits. decodeURIComponent throws; the tool keeps the % as typed and points at it.
  • Broken UTF-8. %E1%BB is the first two bytes of a three-byte character, cut off. That usually means a string was truncated to a byte limit. The tool reports it rather than inventing a character.
  • Double encoding. %25E2%2582%25AC decodes once to %E2%82%AC, which decodes again to €. If you see %25 followed by hex digits in a URL, something encoded an already encoded string. Fix the code that encodes twice; decoding twice "to be safe" breaks values that genuinely contain %.

Common mistakes

  1. Using `encodeURI` on a value. It leaves & = ? # alone, so values containing them corrupt the query.
  2. Concatenating raw text into a URL. "?q=" + text works until someone types &, # or +.
  3. Encoding a whole URL with `encodeURIComponent` and then using it as a link. The :// becomes %3A%2F%2F and the browser treats it as a relative path.
  4. Decoding twice. It turns a legitimate %25 into %, and a value like 100%25 into an error.
  5. Assuming `+` always means space. Only in form-encoded queries.

If you are building campaign links, the UTM link builder encodes every value for you and keeps an existing query and fragment intact; UTM naming conventions covers what to put in those values. For anything else, paste the URL into the URL encoder and decoder and read it part by part before you ship it.

Tools mentioned in this article

Back to the blog