You paste a URL into your browser, hit Enter, and suddenly it's full of %20, %3F, and %26. Or worse — your API call fails because a query parameter contains a space or an & you didn't encode.
URL encoding trips up everyone eventually. The good news: it's much simpler than it looks. This is a practical guide to reading percent-encoded URLs, knowing when encoding happens automatically, and fixing the bugs it causes.
Why URL encoding exists at all
A URL has to be plain ASCII — but the real world isn't. Spaces, emoji, Chinese characters, ampersands... all of them need to travel through a URL without breaking its structure.
The solution: any character that isn't URL-safe gets replaced with a % followed by its hexadecimal byte value.
- A space becomes
%20(hex 20 = 32 = space in ASCII) -
&becomes%26 -
?becomes%3F
The ? and & are the interesting ones — because those characters do have a special job in URLs. That's the key to understanding the whole system.
The two categories you must not confuse
Every character in a URL falls into one of two buckets:
1. Reserved characters — they are part of the URL's structure:
: / ? # [ ] @ ! $ & ' ( ) * + , ; =
These have meaning: ? starts the query string, & separates parameters, # starts the fragment. If your data contains one of these characters, it must be encoded — otherwise the URL's structure gets corrupted.
2. Unsafe / non-ASCII characters — spaces, control characters, non-Latin text, emoji. They must also be encoded because URLs are transmitted as ASCII.
Everything else (letters, digits, -, _, ., ~) passes through untouched.
That's the entire mental model. When you see %26 in a URL, it doesn't mean "the website hates you" — it means the original data contained a literal & that had to be protected so it wouldn't be mistaken for a parameter separator.
The cheat sheet you'll actually use
| Encoded | Character | Where you'll meet it |
|---|---|---|
%20 |
space | Search queries, file names |
%2F |
/ |
Paths inside query params |
%3F |
? |
A literal ? in data |
%26 |
& |
Values containing &
|
%3D |
= |
Values containing =
|
%25 |
% |
The classic double-encode bug |
%2B |
+ |
Legacy form encoding of spaces |
About %25: this is my favorite interview question. If you see %2520 in a URL, the original data was %20 — someone encoded an already encoded string. The % became %25, leaving 20 intact. Double-encoding bugs almost always trace back to a URL being encoded twice by two different layers (your code + a library).
When encoding happens automatically (and when it doesn't)
This is where bugs sneak in. The browser encodes some things for you, but not everything, and not everywhere.
The browser address bar is forgiving. Type https://example.com/search?q=hello world and the browser will quietly encode the space for you. This hides bugs during manual testing that blow up when the same URL is requested programmatically.
fetch() and <a href> are NOT forgiving in the same way. Spaces and non-ASCII characters in a query string can produce malformed requests or, worse, silently work in one browser and fail in another.
Form submissions (GET forms) encode spaces as +. That's why hello+world and hello%20world can both mean "hello world", depending on which layer produced them. This is also why decoding a form-encoded string with the wrong decoder gives you literal + signs scattered through your data.
The JavaScript functions, once and for all
In JavaScript you have four tools, and picking the wrong one is a rite of passage:
// encodeURIComponent — for a VALUE going into a query string
encodeURIComponent("fish & chips? yes")
// "fish%20%26%20chips%3F%20yes"
// ✅ Encodes & and ? — the structure is protected
// encodeURI — for a WHOLE URL that's already assembled
encodeURI("https://example.com/search?q=hello world")
// "https://example.com/search?q=hello%20world"
// ✅ Leaves :// and ? alone, encodes the space
// decodeURIComponent / decodeURI — the reversals
decodeURIComponent("fish%20%26%20chips")
// "fish & chips"
The rule of thumb:
-
Encoding one value (about to insert it into a query string)? →
encodeURIComponent -
Encoding a complete URL that's already well-formed except for stray spaces/unicode? →
encodeURI
The mistake I see most often: using encodeURI on a value. It leaves & and ? untouched, so fish & chips slips into the URL as-is and silently truncates your query string at the &.
// ❌ Broken: value contains & and ?
"https://example.com/q?" + encodeURI("fish & chips?")
// ✅ Correct
"https://example.com/q?" + encodeURIComponent("fish & chips?")
Three real bugs you'll eventually meet
1. The truncated query string. A search for Tom & Jerry becomes ?q=Tom and everything after & becomes a new (empty) parameter. The page loads, the search just returns wrong results. Maddening until you look at the Network tab.
2. The double-encoded redirect. You pass a ?next= parameter containing a full URL. Some middleware encodes it again. The redirect target now contains %253A instead of %3A. The fix is to decode exactly once at each layer, and to be disciplined about which layer owns encoding.
3. The emoji that kills your sitemap. Non-ASCII characters in URLs are legal (via encoding), but if your CMS or export script writes raw UTF-8 into an XML sitemap without encoding, Google may start complaining. Always encode before writing URLs into structured files.
When you just need to check one string quickly
For debugging, I keep a browser-side URL encoder/decoder bookmarked — paste the string, see it encoded and decoded side by side, copy whichever form you need. It runs client-side, so tokens and internal URLs never leave the browser. Pair it with console.log and the Network tab and you can diagnose 95% of encoding bugs in a minute.
The 30-second summary
-
%XXis just a character's hex byte value, prefixed with% - Reserved characters (
& ? = / #) must be encoded when they're data, not structure - The address bar hides encoding bugs; test with
fetchor curl -
encodeURIComponentfor values,encodeURIfor whole URLs -
%25appearing where%20should be means something double-encoded
URL encoding stops being scary the moment you stop reading %3F as noise and start reading it as "that's a literal question mark in the data". Once that clicks, every encoded URL you see becomes just... a string with a hat on.
Questions or war stories about encoding bugs?
Top comments (0)