URL encoding trips up developers more often than it should. Not because it's complicated — it isn't — but because there are two different standards, multiple languages implement them slightly differently, and the difference between %20 and + for a space has caused real production bugs.
Let me break it down clearly.
Why URLs Need Encoding
HTTP URLs can only safely contain a subset of ASCII characters: letters, digits, and a handful of symbols (-._~:/?#[]@!$&'()*+,;=). Everything else — spaces, accented characters, &, =, #, and non-ASCII text — must be percent-encoded.
Percent-encoding replaces the unsafe character with % followed by the character's UTF-8 byte value in hexadecimal:
- Space →
%20 & → %26= → %3Dé → %C3%A9 (two bytes in UTF-8)
Simple enough. The confusion starts when you need to choose which kind of encoding to apply.
encodeURIComponent vs encodeURI: The Core Distinction
JavaScript gives you two built-in functions, and mixing them up is the #1 URL encoding mistake:
encodeURIComponent(value) encodes everything except: A-Z a-z 0-9 - _ . ! ~ * ' ( )
That includes /, ?, =, &, #, @, and : — all URL structural characters.
encodeURI(url) encodes everything except URL structural characters: : / ? # [ ] @ ! $ & ' ( ) * + , ; =
The rule
- Use
encodeURIComponent for individual query parameter values - Use
encodeURI for complete URLs where you only want to encode unsafe characters without touching the URL's structure
``javascript
// Correct — encoding a query value
const query = "coffee & cake";
const url = https://example.com/search?q=${encodeURIComponent(query)};
// → https://example.com/search?q=coffee%20%26%20cake
// Wrong — encodeURI doesn't encode & so the parameter breaks
const url2 = https://example.com/search?q=${encodeURI(query)};
// → https://example.com/search?q=coffee%20&%20cake ← broken!
`
The %20 vs + Space Confusion
You'll see both %20 and + used for spaces in URLs. They're not interchangeable:
| Form | Standard | Valid In |
|------|----------|----------|
| %20 | RFC 3986 percent-encoding | All URL contexts |
| + | HTML form encoding (application/x-www-form-urlencoded) | Query strings only |
%20 is always correct. + is only correct in query strings when the server expects form-data encoding.
This matters when you're encoding a redirect URL as a parameter — a + inside a URL value that gets passed to another server and decoded as RFC 3986 will stay as a literal +, not become a space.
Language Cheat Sheet
JavaScript / Node.js:
`javascript
// Query parameter values
encodeURIComponent("hello world & more") // hello%20world%20%26%20more
// Query strings with URLSearchParams
new URLSearchParams({ q: "hello world" }).toString() // q=hello+world (+ for spaces)
`
Python:
`python
from urllib.parse import quote, urlencode
quote("hello world & more", safe="") # hello%20world%20%26%20more
urlencode({"q": "hello world"}) # q=hello+world (form encoding)
`
Java:
<code>java
<p>// URLEncoder uses + for spaces — this is form encoding, not RFC 3986</p>
<p>URLEncoder.encode("hello world", StandardCharsets.UTF_8) // hello+world</p>
<p>// Replace + with %20 if you need RFC 3986:</p>
<p>encoded.replace("+", "%20")</p>
</code>
C#:
`csharp
// Use Uri.EscapeDataString — produces %20 for spaces (RFC 3986)
Uri.EscapeDataString("hello world & more") // hello%20world%20%26%20more
// Avoid HttpUtility.UrlEncode — uses + for spaces
`
curl:
<code>bash
<p>curl -G --data-urlencode "q=hello world & more" https://example.com/search</p>
</code>
Double-Encoding: The Silent Bug
If you encode an already-encoded string, the % sign itself gets encoded to %25:
<code>
<p>%20 (encoded space)</p>
<p>↓ encode again</p>
<p>%2520 (which decodes to "%20", not a space)</p>
</code>
This is a common source of "double-encoded" URLs that confuse both users and servers. If your encoded string looks like %2520 instead of %20, you have a double-encoding bug.
Fix: always decode before re-encoding if the string might already be encoded.
When a URL Is a Parameter Value
This is the trickiest case — a redirect URL embedded inside another URL:
<code>javascript
<p>// Inner URL must be fully encoded so its ? & = / don't break the outer URL</p>
<p>const innerUrl = "https://destination.com/path?key=value&other=data";</p>
<p>const outerUrl = </code>https://proxy.example.com/redirect?target=${encodeURIComponent(innerUrl)}<code>;</p>
<p>// → https://proxy.example.com/redirect?target=https%3A%2F%2Fdestination.com%2Fpath%3Fkey%3Dvalue%26other%3Ddata</p>
</code>
Only encode the value — not the outer URL's structure.
Quick Reference
Common characters and their encoded forms:
| Character | Encoded | Notes |
|-----------|---------|-------|
| Space | %20 | + only in form data |
| & | %26 | Query string separator — always encode in values |
| = | %3D | Key-value separator — encode in values |
| ? | %3F | Query start — encode in values |
| / | %2F | Path separator — encode in values |
| # | %23 | Fragment — encode in values |
| + | %2B | Encode in values (means space in form encoding) |
| % | %25 | Encode existing % to avoid double-encoding |
If you're working with URLs in the browser, there's a free URL Encoder / Decoder that handles both encodeURIComponent and encodeURI` modes, plus batch processing for encoding lists of values at once — all client-side, so your URLs never leave the browser.