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 & chips must 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-urlencoded encode 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%20chips

Rule 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%3Dsummary

Notice 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 %20 when 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