How Percent-Encoding Works
Characters not allowed in URLs are converted to %XX format, where XX is the hexadecimal byte value.
Character: space é 中
UTF-8 bytes: 0x20 0xC3 0xA9 0xE4 0xB8 0xAD
Encoded: %20 %C3%A9 %E4%B8%AD
Multi-byte characters produce multiple %XX sequences
Character Categories
| Category |
Characters |
Encoding Required? |
| Unreserved |
A-Z a-z 0-9 - . _ ~ |
Never encode |
| Reserved (gen-delims) |
: / ? # [ ] @ |
Encode when used as data |
| Reserved (sub-delims) |
! $ & ' ( ) * + , ; = |
Encode when used as data |
| Unsafe |
space " < > { } | \ ^ ` |
Always encode |
| Non-ASCII |
All characters above 0x7F |
Always encode |
Common Character Encodings
JavaScript Encoding Functions
| Function |
Purpose |
Preserves |
Use For |
encodeURIComponent() |
Encode a URI component |
A-Za-z0-9 - _ . ! ~ * ' ( ) |
Query values, path segments |
encodeURI() |
Encode a complete URI |
A-Za-z0-9 ; , / ? : @ & = + $ - _ . ! ~ * ' ( ) # |
Full URLs (rarely needed) |
decodeURIComponent() |
Decode a URI component |
— |
Decoding query values |
decodeURI() |
Decode a complete URI |
— |
Decoding full URLs |
// Encoding examples
encodeURIComponent("hello world") → "hello%20world"
encodeURIComponent("a=1&b=2") → "a%3D1%26b%3D2"
encodeURIComponent("user@example.com") → "user%40example.com"
encodeURI("https://example.com/path?q=hello world")
→ "https://example.com/path?q=hello%20world"
// Decoding examples
decodeURIComponent("hello%20world") → "hello world"
decodeURIComponent("%E4%B8%AD%E6%96%87") → "中文"
Common encoding/decoding operations
Space Encoding: %20 vs +
Spaces can be encoded two ways depending on context:
| Encoding |
Context |
Standard |
Example |
%20 |
URL path, most contexts |
RFC 3986 |
/hello%20world |
+ |
HTML form data (query strings) |
application/x-www-form-urlencoded |
?name=hello+world |
// URLSearchParams uses + for spaces
new URLSearchParams({name: "hello world"}).toString()
→ "name=hello+world"
// encodeURIComponent uses %20
encodeURIComponent("hello world")
→ "hello%20world"
// Both decode correctly
decodeURIComponent("hello+world") → "hello+world" (+ preserved!)
decodeURIComponent("hello%20world") → "hello world"
// Use URLSearchParams for form data
const params = new URLSearchParams("name=hello+world");
params.get("name") → "hello world"
Space encoding behavior differences
International Characters (UTF-8)
Non-ASCII characters are first converted to UTF-8 bytes, then percent-encoded:
| Character |
Unicode |
UTF-8 Bytes |
Percent-Encoded |
| é |
U+00E9 |
C3 A9 |
%C3%A9 |
| ñ |
U+00F1 |
C3 B1 |
%C3%B1 |
| 中 |
U+4E2D |
E4 B8 AD |
%E4%B8%AD |
| 日 |
U+65E5 |
E6 97 A5 |
%E6%97%A5 |
| 🎉 |
U+1F389 |
F0 9F 8E 89 |
%F0%9F%8E%89 |
Encoding Rules by Component
| Component |
Must Encode |
Safe Characters |
| Path segment |
Reserved chars if used as data |
unreserved + : @ ! $ & ' ( ) * + , ; = |
| Query key |
= & and reserved if data |
unreserved + : @ ! $ ' ( ) * + , ; / |
| Query value |
= & and reserved if data |
unreserved + : @ ! $ ' ( ) * + , ; / ? |
| Fragment |
Non-URI characters |
unreserved + : @ ! $ & ' ( ) * + , ; = / ? |
| Userinfo |
@ : and reserved |
unreserved + ! $ & ' ( ) * + , ; = |
Common Encoding Mistakes
| Mistake |
Problem |
Correct Approach |
| Double encoding |
%25252F instead of %2F |
Encode once, at the boundary |
| Using encodeURI() for values |
Doesn't encode = & |
Use encodeURIComponent() |
| Manual string building |
Missing edge cases |
Use URLSearchParams or URL API |
| Encoding already-encoded |
% becomes %25 |
Decode first if unsure |
Not encoding + |
Interpreted as space in queries |
Encode as %2B |