Why URLs can't just contain any character
URLs are restricted to a specific set of characters by RFC 3986 — unreserved characters (A-Z a-z 0-9 - _ . ~) can appear literally; everything else, if it needs to appear as data rather than as URL syntax, must be percent-encoded: replaced with % followed by its byte value in hex. A raw space, an ampersand inside a value, or a non-ASCII character would otherwise be ambiguous or simply invalid in a URL.
"hello world" → hello%20world
"a&b" → a%26b
"café" → caf%C3%A9 (UTF-8 bytes, each percent-encoded)
Reserved characters: sometimes syntax, sometimes data
Some characters are reserved because they have structural meaning in a URL: / separates path segments, ? starts the query string, & separates query parameters, = separates a parameter's key from its value, # starts the fragment. The encoding rule depends on context: if you need one of these characters as data (a literal & inside a search query value, for instance), it must be percent-encoded (%26); if it's acting as actual URL structure, it should not be encoded, or the URL's meaning changes.
?q=cats%26dogs → the query VALUE is "cats&dogs" (one parameter)
?q=cats&dogs=1 → TWO separate parameters: q=cats, dogs=1
This distinction — data vs. structure — is the single most important thing to get right, and mixing it up either breaks the URL's structure or corrupts the data it carries.
encodeURI vs. encodeURIComponent
JavaScript ships two different encoding functions for exactly this reason, and using the wrong one is a common source of bugs:
| Function | Encodes reserved characters (/ ? & = #)? |
Use for |
|---|---|---|
encodeURI() |
No — leaves URL structure intact | Encoding a whole URL |
encodeURIComponent() |
Yes — encodes everything except unreserved characters | Encoding a single value going into a URL (a query parameter, a path segment) |
Using encodeURI() on a single query parameter value fails to escape & and =, meaning a value like "a&b=c" would corrupt the query string it's inserted into by silently adding parameters that were never intended. encodeURIComponent() is almost always the right choice when encoding a value, not a full URL.
Double-encoding: the classic production bug
Applying percent-encoding twice produces a subtly wrong, but not obviously broken, result:
Original: "100% off"
Encoded once: 100%25%20off
Encoded twice: 100%2525%2520off (the % from the first encoding gets re-encoded)
This typically happens when a value is encoded once by application code, then encoded again by a framework, proxy, or browser that assumes the input wasn't already encoded. The symptom is usually a literal %25 appearing where a % was expected, or a link that "almost" works but resolves to the wrong resource. The fix is always to encode exactly once, at exactly one layer, and pass already-encoded values through unchanged elsewhere in the pipeline.
Common mistakes
- Using
encodeURIwhen you meanencodeURIComponent(or the equivalent in another language), under-encoding reserved characters inside a parameter value. - Encoding twice across layers that both assume responsibility for it. Decide once where encoding happens in your request pipeline, and don't re-encode already-encoded values downstream.
- Forgetting
+means literal space in query strings specifically, not%20— an artifact of the olderapplication/x-www-form-urlencodedconvention still used for form submissions and some query strings, distinct from path encoding where+is just a literal plus sign. - Not encoding non-ASCII characters at all, producing a URL that "looks fine" in a browser address bar (which decodes for display) but fails when copy-pasted into a strict parser or an older system.
FAQ
What's the difference between encodeURI and encodeURIComponent?encodeURI preserves reserved characters (/ ? & = #) because it assumes you're encoding a full URL; encodeURIComponent encodes everything except unreserved characters, for encoding one value that will be inserted into a URL.
Why does + sometimes mean a space and other times mean a literal plus?
In application/x-www-form-urlencoded data (typical form submissions and many query strings), + is a legacy convention for space; in the general URL path, + is just a literal character and space must be %20.
What happens if a value gets URL-encoded twice?
The literal % characters from the first encoding pass get re-encoded to %25, producing a value that decodes back to something like %20 instead of an actual space — a common, hard-to-spot bug.
Do I need to encode a URL that only contains letters and numbers?
No — unreserved characters (letters, digits, - _ . ~) never need encoding; encoding is only required for reserved, unsafe, or non-ASCII characters.
Encode, decode, and parse URLs and query parameters instantly with the URL Encoder/Decoder and URL Parser — everything runs client-side.