You send a request, the server chokes, and half an hour later you discover a space in a query parameter was the culprit. URL encoding is one of those things that sits quietly in the background until it breaks something at the worst possible time. Understanding it properly saves a lot of guesswork.

What URL encoding is

A URL can only safely contain a limited set of characters — letters, digits, and a handful of symbols. Everything else has to be represented using percent-encoding: a % followed by two hexadecimal digits that represent the byte value of the character.

A space becomes %20. An ampersand becomes %26. A forward slash becomes %2F. The encoding is defined in RFC 3986, and browsers, HTTP clients, and servers all speak it.

The reason it exists is simple: certain characters have structural meaning in a URL. The ? starts the query string, & separates parameters, = assigns values, # starts a fragment. If your actual data contains any of those characters, you have to escape them so they aren't mistaken for structure.

When you actually need it

Most of the time your framework or HTTP library handles this for you. But you'll run into it directly when:

  • Building a query string by hand — especially with user input, search terms, or JSON payloads
  • Passing a full URL as a parameter to another URL (redirect URLs, OAuth callbacks)
  • Constructing paths that contain characters like spaces, slashes, or non-ASCII text
  • Debugging a request where something arrives mangled and you need to see what the raw bytes are

The redirect-URL case is the classic trap. If you drop a URL into a query parameter without encoding it, its own ? and & characters get read as part of the outer URL, and everything after the first one silently disappears.

The two flavours you'll meet in JavaScript

JavaScript gives you two encoding functions, and mixing them up is a common source of bugs.

encodeURIComponent

This is the one you want most of the time. It encodes everything that isn't safe, including /, ?, &, and =. Use it for individual query values and path segments.

encodeURI

This is for encoding a whole URL. It leaves structural characters like /, ?, and & alone because it assumes they're meant to be there. If you use it on a single parameter value, it won't escape enough.

const term = "cats & dogs?";

// Correct: encode the individual value
const url = `https://api.example.com/search?q=${encodeURIComponent(term)}`;
// https://api.example.com/search?q=cats%20%26%20dogs%3F

// Wrong: & and ? survive and break the query structure
const broken = `https://api.example.com/search?q=${encodeURI(term)}`;
// https://api.example.com/search?q=cats%20&%20dogs?

The gotchas that get people

The plus-sign problem

In the application/x-www-form-urlencoded format — used by HTML forms and often by query strings — a space is encoded as +, not %20. But in the path portion of a URL, + is a literal plus sign. So %2B means a plus, + in a query means a space, and + in a path means a plus. If you're decoding a query string, remember to convert + back to a space; if you're decoding a path, don't.

Double encoding

Encode a string twice and you get garbage. A space becomes %20, then the % itself gets encoded to %25, giving you %2520. This happens a lot when a value passes through two layers that both try to be helpful — say, a framework that encodes automatically and code that also encodes manually. The symptom is literal %20 text showing up on the page. When you see %25 where you didn't expect it, suspect double encoding.

Non-ASCII characters

Percent-encoding works on bytes, not characters. Modern URLs use UTF-8, so a character like é becomes two encoded bytes: %C3%A9. If a system assumes a different encoding, you get mojibake. Stick to UTF-8 everywhere and this mostly disappears.

Encoding things that don't need it

Over-encoding is usually harmless but occasionally breaks equality checks or signature verification. Some APIs sign the exact bytes of a URL, so %7E versus a literal ~ can produce different signatures even though both are technically valid. When an API is fussy about signatures, encode exactly what its documentation says and nothing more.

A worked example

Say you're building an OAuth redirect and need to pass a callback URL through as a parameter:

const callback = "https://myapp.com/auth/callback?session=abc&next=/dashboard";

const authUrl =
  "https://provider.com/authorize" +
  "?client_id=1234" +
  "&redirect_uri=" + encodeURIComponent(callback);

// redirect_uri becomes:
// https%3A%2F%2Fmyapp.com%2Fauth%2Fcallback%3Fsession%3Dabc%26next%3D%2Fdashboard

Because the whole callback URL — including its own query string — is encoded as a single component, the provider receives it intact and hands it back to you unchanged. Skip that encodeURIComponent and the provider would only see up to the first &.

Debugging encoded strings

When you're staring at a wall of percent signs trying to work out whether something is encoded once, twice, or not at all, it helps to have a quick way to flip between the raw and encoded forms. I keep a URL Encoder / Decoder open in a tab for exactly this — paste in the mangled value, decode it, and immediately see whether you're dealing with a double-encoding issue or a genuinely broken string.

Rules of thumb

  • Use encodeURIComponent for individual parameter values and path segments
  • Use encodeURI only for encoding an entire URL that's otherwise already correct
  • Never encode the same string twice — let one layer own the encoding
  • Watch + in query strings: it means space in form-encoded data
  • Keep everything UTF-8 to avoid character corruption

Encoding isn't complicated once the model clicks: escape anything that would otherwise be read as structure, decode it on the other side, and don't do it twice. Most URL bugs are one of those three principles being quietly violated somewhere in the chain.

Photo by Zulfugar Karimov on Unsplash