A URL can only contain a small, fixed set of characters safely — letters, digits, and a handful of punctuation marks. Everything else (spaces, &, #, non-English characters, emoji) has to be represented some other way, or it either breaks the URL or gets silently misinterpreted. That "some other way" is percent-encoding, and most of the confusing bugs around it come from not knowing which of several slightly different encoding rules is being applied at any given moment.
What Percent-Encoding Actually Does
Percent-encoding replaces a character with % followed by its two-digit hex byte value. A space becomes %20 because 32 (the space character's ASCII code) is 20 in hex. An & becomes %26. Non-ASCII characters get encoded as their UTF-8 bytes, so é becomes %C3%A9 (two bytes, two %XX pairs).
The rules split characters into two groups:
- Unreserved characters — letters, digits,
-,.,_,~— never need encoding and are left alone. - Reserved characters —
: / ? # [ ] @ ! $ & ' ( ) * + , ; =— have special meaning within a URL (separating the path from the query, one parameter from another, and so on). Whether they need encoding depends on where they appear.
That last point is the source of most confusion: the same character is sometimes reserved and sometimes not, depending on context.
Why %20 and + Both Mean "Space"
This is the single most common source of confusion. There are two legitimate encodings for a space, from two different eras of the web:
%20is the correct percent-encoding for a space anywhere in a URL — path, query string, anywhere.+means "space" specifically inside a query string encoded asapplication/x-www-form-urlencoded— the format browsers have used for HTML form submissions since the 1990s.
The problem is that + is also a valid, literal character that can appear in data (a + in a phone number, for instance). If you decode a query string with a form-decoder, + becomes a space. If you decode the same string with a strict URI decoder, + stays a literal +. Get the wrong one and a search query like c++ either survives correctly or silently turns into c (two spaces). Neither decoder is "wrong" — they're answering different questions.
encodeURI vs encodeURIComponent: Not Interchangeable
JavaScript ships two built-in encoders that look similar but solve different problems:
encodeURIComponent()encodes almost everything except unreserved characters — including/,&,?, and=. Use this for a single value you're about to insert into a URL, like a query parameter:encodeURIComponent("a&b")→a%26b.encodeURI()assumes you're encoding an entire URL that already has its structure in place, so it leaves/,&,?,=, and:alone — encoding those would break the URL's structure. It only encodes things like spaces and non-ASCII characters.
Using encodeURI() on a single query parameter is a common bug: if the value itself contains & or =, they pass through unencoded and get misread as the start of a new parameter, corrupting the query string.
Double Encoding: The %2520 Bug
If you encode a string that's already encoded, the % from the first encoding pass gets encoded itself — % is %25 in hex — turning %20 into %2520. This happens constantly in real systems: a value gets encoded once when a form is submitted, then encoded again by a proxy, a redirect handler, or a second layer of application code that assumes it's still raw.
The symptom is always the same: what should decode to a clean value instead decodes to something with stray %25 sequences still in it, requiring a second decode pass to fix. The fix isn't a smarter decoder — it's finding where in the pipeline the value got encoded twice and removing one of those steps.
Quick FAQ
Should I use %20 or + for spaces?
Use %20 unless you're specifically building a application/x-www-form-urlencoded query string (the default format for HTML GET forms), where + is conventional. When in doubt, %20 is unambiguous in every context.
Why does my decoded URL still have % signs in it? It's almost certainly double-encoded — decode it once more. If this keeps happening, the real fix is to find and remove the extra encoding step upstream rather than adding a permanent second decode.
Do I need to encode an entire URL, or just parts of it?
Encode each dynamic value on its own with encodeURIComponent() before inserting it into the URL template. Encoding the whole finished URL at once is what causes structural characters like / and & to either get wrongly encoded or wrongly left alone.
Why do non-English characters turn into long %XX chains?
Percent-encoding operates on UTF-8 bytes, not characters. Most non-ASCII characters take 2–4 bytes in UTF-8, so each one becomes 2–4 separate %XX groups.
Try It Yourself
- URL Encoder / Decoder — encode or decode a string instantly, free
Related Free Tools
- Base64 Encoding Explained — another encoding that trips people up in similar ways
- JWT Decoder — decode a JSON Web Token in your browser
- JSON Formatter — validate and pretty-print JSON