URL encoding is one of those things you use every day without thinking about it, until a query parameter breaks and you spend an hour figuring out why + turned into a space. It's worth understanding properly, because the rules are subtle and the failure modes are annoying.

What URL encoding actually is

A URL can only reliably contain a limited set of characters: letters, digits, and a handful of symbols. Everything else — spaces, ampersands, question marks, non-ASCII characters — has to be represented some other way so it doesn't clash with the URL's own structure. That mechanism is percent-encoding.

The rule is simple: take the byte, write it as two hex digits, and prefix it with %. A space becomes %20. An ampersand becomes %26. The letter é (UTF-8 bytes 0xC3 0xA9) becomes %C3%A9.

Characters split into two groups. Reserved characters have special meaning in a URL — things like /, ?, #, &, =, and :. Unreserved characters (A–Z, a–z, 0–9, and -, _, ., ~) are always safe. When a reserved character needs to appear as literal data rather than as a delimiter, you encode it.

When and why you'd use it

The most common case is building query strings. Say you want to search for the phrase "cats & dogs". If you drop it straight into a URL like this:

https://example.com/search?q=cats & dogs

...you've broken it in two ways. The space is invalid, and the & will be read as a parameter separator, so the server thinks you passed q=cats and a second empty parameter called dogs. Encoded correctly, it becomes:

https://example.com/search?q=cats%20%26%20dogs

Other places you'll reach for encoding:

  • Putting user-supplied values into path segments (a filename with spaces, a slug with slashes).
  • Passing a full URL as a parameter — like a redirect target or an OAuth redirect_uri.
  • Sending form data as application/x-www-form-urlencoded.
  • Embedding tokens or JSON blobs in a link.

The gotchas that bite people

Encoding the whole URL vs. the parts

You almost never want to encode an entire URL in one go. If you do, you'll turn https:// into https%3A%2F%2F and destroy the structure. Encode the individual pieces — each query value, each path segment — then assemble them.

encodeURI vs. encodeURIComponent

JavaScript gives you two functions and picking the wrong one is a classic mistake. encodeURI assumes you're passing a complete URL and leaves reserved characters like &, =, and / alone. encodeURIComponent encodes them, which is what you want for a single parameter value.

const target = "https://app.example.com/dashboard?tab=1";

// Wrong: reserved chars survive and break the outer URL
encodeURI(target);
// "https://app.example.com/dashboard?tab=1"

// Right: safe to drop into ?next=
encodeURIComponent(target);
// "https%3A%2F%2Fapp.example.com%2Fdashboard%3Ftab%3D1"

const redirect = `https://auth.example.com/login?next=${encodeURIComponent(target)}`;

The + and space confusion

In the query-string portion, a literal space is sometimes encoded as + (the application/x-www-form-urlencoded convention) and sometimes as %20 (proper percent-encoding). They are not interchangeable everywhere. encodeURIComponent produces %20 and leaves any literal + alone — which means a genuine plus sign in your data can be misread as a space by some decoders. If you're passing something like a phone number or a base64 string containing +, encode it explicitly.

Double encoding

This one hides in layered systems. A value gets encoded, passed to another service, that service encodes it again, and now %20 has become %2520 (the % itself got encoded). Users see literal %20 in the page. The fix is discipline: encode exactly once, at the boundary where you build the URL, and decode exactly once when you read it.

Non-ASCII and normalization

Percent-encoding operates on bytes, so the character encoding matters. Modern URLs use UTF-8, but if some part of your stack assumes a different encoding you'll get mojibake. Encode using UTF-8 end to end and this mostly takes care of itself.

A short worked example

Imagine building a link to prefill a support form with the user's message and email.

const email = "[email protected]";
const message = "Order #42 arrived damaged — need a refund & replacement";

const params = new URLSearchParams({
  email,
  message,
});

const url = `https://support.example.com/new?${params.toString()}`;
console.log(url);

Using URLSearchParams is the pragmatic move here — it handles the encoding for each key and value for you, so you don't hand-roll it. The output looks like:

https://support.example.com/new?email=jane%2Bwork%40example.com&message=Order+%2342+arrived+damaged+%E2%80%94+need+a+refund+%26+replacement

Notice the + in the email became %2B, the @ became %40, and the ampersand in the message became %26 so it isn't read as a delimiter. The em dash was UTF-8 encoded to %E2%80%94. That's exactly the behavior you want.

Debugging encoded strings

When something looks off in a URL, the fastest way to check is to decode it and see what you actually get back. I keep a URL Encoder / Decoder open for this — paste in a mangled query value, decode it, and the double-encoding or wrong-function bug usually jumps right out. It beats squinting at %2520 in the browser address bar.

Rules of thumb

  • Encode components, not whole URLs.
  • Use encodeURIComponent (or URLSearchParams) for query values, not encodeURI.
  • Encode once, decode once.
  • Keep everything in UTF-8.
  • Watch out for + when your data legitimately contains one.

None of this is complicated once the model clicks: the URL has structure, encoding protects that structure, and your job is just to encode data at the right layer. Get the boundaries right and the bugs mostly disappear.

Photo by Ilya Pavlov on Unsplash