URL encoding is one of those topics everyone assumes they understand until a query parameter with a plus sign silently breaks in production. It's not complicated, but the rules are fiddly and the standards layer on top of each other in confusing ways. Here's the practical version.
What URL encoding actually is
A URL can only safely contain a limited set of characters: letters, digits, and a handful of punctuation marks. Anything outside that set — spaces, ampersands, slashes used as data rather than path separators, non-ASCII text — has to be escaped. That escaping is called percent-encoding, because each disallowed byte is written as a percent sign followed by its two-digit hexadecimal value.
A space becomes %20. An ampersand becomes %26. The letter ü, encoded as UTF-8, becomes %C3%BC — two bytes, two escapes. The key insight: percent-encoding operates on bytes, not characters. Which encoding produced those bytes matters, and these days the answer is almost always UTF-8.
When you actually need it
You need encoding any time you put arbitrary data into a URL. The common cases:
- Query parameters. A search term like
fish & chipsmust be encoded, or the&gets read as a parameter separator. - Path segments. If a filename or ID contains a slash or space, it needs encoding so it doesn't change the route.
- Form submissions. HTML forms with
application/x-www-form-urlencodedencode the whole body using these rules. - Redirect targets. When you pass a full URL as a parameter value (like
?return_to=...), that inner URL has to be encoded so its own query string doesn't leak into the outer one.
The gotchas that bite everyone
Plus signs and spaces
This is the classic. In the query string of a form submission, a space is historically encoded as +, not %20. That means a literal plus sign has to be encoded as %2B, or it'll be decoded back into a space. In the path, however, + means a literal plus and spaces are %20. JavaScript's encodeURIComponent always uses %20, which is safe for both but doesn't match form behaviour. If you're parsing form data, decode + as a space yourself.
Encoding the whole URL vs. a component
JavaScript gives you two functions, and picking the wrong one is a frequent source of bugs. encodeURI is meant for a complete URL and leaves structural characters like :, /, ?, and & alone. encodeURIComponent is for a single piece — a parameter name or value — and escapes those structural characters too.
const term = "fish & chips";
// Wrong: the & survives and corrupts the query
encodeURI(`https://example.com/search?q=${term}`);
// https://example.com/search?q=fish%20&%20chips
// Right: encode the value, not the URL
`https://example.com/search?q=${encodeURIComponent(term)}`;
// https://example.com/search?q=fish%20%26%20chipsRule of thumb: build the URL structure yourself, then run each dynamic value through encodeURIComponent.
Double encoding
If you encode something that's already encoded, %20 becomes %2520 because the percent sign itself gets escaped. This usually happens when a value passes through two layers that both "helpfully" encode. Symptoms: URLs full of %25, or text showing literal %20 on the page. Fix it by being explicit about which layer owns the encoding and only encoding once.
Reserved characters that are sometimes safe
Characters like !, ', (, ), and * are technically reserved but encodeURIComponent doesn't escape them. Most servers handle them fine, but some strict parsers or signature schemes (OAuth 1.0a, AWS request signing) require them encoded. If you're computing a signature over a URL, read the spec carefully — mismatched encoding is the number one cause of signature failures.
A short worked example
Say you're building a link that sends users back to a specific page after login, and that page itself has query parameters:
const returnTo = "https://app.example.com/report?id=42&tab=summary";
const loginUrl =
"https://auth.example.com/login?return_to=" +
encodeURIComponent(returnTo);
// https://auth.example.com/login?return_to=https%3A%2F%2Fapp.example.com%2Freport%3Fid%3D42%26tab%3DsummaryNotice everything in the inner URL is escaped: the colons as %3A, slashes as %2F, the question mark as %3F, the ampersand as %26. That's exactly what you want — the whole thing is now a single, opaque parameter value. On the auth server you simply read the return_to parameter and the standard URL parsing decodes it back to the original. If you had skipped encoding, the &tab=summary would have been parsed as a separate parameter of the login URL and lost.
Decoding, and checking your work
Decoding reverses the process: find each %XX, convert the hex to a byte, and interpret the resulting byte stream as UTF-8. In JavaScript that's decodeURIComponent. One thing to watch: decoding can throw if the string contains a malformed sequence like a lone % or %ZZ. Wrap it in a try/catch when the input comes from users or external systems.
When I'm debugging a mangled URL, I don't trust my own mental hex table — I paste it into a tool and see the decoded result instantly. Our URL Encoder / Decoder handles both directions and is handy for spotting double-encoding at a glance: if you decode once and still see % sequences, you know something encoded it twice.
Quick checklist
- Encode values, not whole URLs, with
encodeURIComponent. - Assume UTF-8 everywhere unless you have a very good reason not to.
- Watch for
+vs%20when dealing with form-encoded data. - Encode exactly once — track which layer owns it.
- For signatures and strict APIs, check the spec's exact encoding rules.
Get these right and a whole category of "why is my parameter missing" bugs just disappears. It's boring plumbing, but plumbing you only have to learn once.
Photo by Jackson Sophat on Unsplash