Skip to main content

URL Encoding vs Percent Encoding: When to Use encodeURI, encodeURIComponent, and Form Encoding

Percent encoding, encodeURI, encodeURIComponent, and form encoding are four different things. Learn which one to use where, with real inputs and outputs.

Published By Li Lei
#url-encoding #percent-encoding #javascript #developer-tools

URL Encoding vs Percent Encoding: When to Use encodeURI, encodeURIComponent, and Form Encoding

"Just URL-encode it" is advice that has broken more query strings than it has fixed. The problem is that "URL encoding" is an umbrella term covering at least three distinct dialects: percent encoding as defined by RFC 3986, JavaScript's two built-in functions (encodeURI and encodeURIComponent), and the older application/x-www-form-urlencoded format that HTML forms still use today. They agree on most characters and disagree on exactly the ones that cause production bugs: spaces, plus signs, ampersands, and slashes. This post walks through what each one actually does, with real inputs and outputs you can reproduce, and ends with a short decision rule you can apply without thinking.

Percent encoding is the mechanism, "URL encoding" is the slang

RFC 3986, the URI standard, defines exactly one escaping mechanism: take the byte, write it as % followed by two hex digits. A space becomes %20. An ampersand becomes %26. Characters outside ASCII are first serialized as UTF-8 bytes, then each byte is escaped, so é (two bytes in UTF-8) becomes %C3%A9 and (three bytes) becomes %E2%82%AC.

That is percent encoding. Everything else — encodeURI, encodeURIComponent, form encoding — is a policy deciding which characters get percent-encoded and which are left alone. The mechanism never changes; the character sets do. Once you see it that way, the whole topic collapses into one question: which policy matches the context I'm writing into?

RFC 3986 splits characters into two camps. Unreserved characters (letters, digits, -, ., _, ~) never need escaping anywhere. Reserved characters (:/?#[]@!$&'()*+,;= — 18 of them in the spec's two reserved sets) are the delimiters that give a URL its structure. Whether a reserved character needs escaping depends entirely on whether you are using it as a delimiter or as data. The & between query parameters is structure; the & inside a search term is data and must become %26.

encodeURI vs encodeURIComponent: an 11-character difference that matters

JavaScript ships both policies as built-ins, and the ECMAScript specification pins their behavior down to exact character lists. encodeURIComponent leaves precisely 71 characters unescaped: the 52 ASCII letters, 10 digits, and the 9 marks - _ . ! ~ * ' ( ) (per the ECMA-262 URI handling section). encodeURI leaves those same 71 plus 11 more — ; / ? : @ & = + $ , # — for a total of 82. Those extra 11 are exactly the characters that act as URL structure.

The consequence is simple:

  • encodeURI assumes you are handing it a whole URL and politely refuses to touch the delimiters, so the URL keeps working as a URL.
  • encodeURIComponent assumes you are handing it one value that will be inserted between delimiters, so it escapes everything structural.

Here is the same string through both, which you can paste into any browser console:

encodeURI("coffee & cream 50%")
// → "coffee%20&%20cream%2050%25"   (& survives — dangerous as a value)

encodeURIComponent("coffee & cream 50%")
// → "coffee%20%26%20cream%2050%25" (& becomes %26 — safe as a value)

If that string is a search term going into ?q=, the encodeURI version silently truncates your data: the server sees q=coffee and a mystery parameter named cream 50%. This single confusion is behind a huge share of "my query parameter loses everything after the ampersand" bug reports.

A rule I give every junior developer: you almost never want encodeURI. It only makes sense when you receive an already-assembled URL that may contain raw spaces or non-ASCII characters and you need to make it transmittable without re-parsing it. For everything you build yourself, encode each component with encodeURIComponent, then join with literal delimiters.

Form encoding is a third dialect, and + is its trap

application/x-www-form-urlencoded predates RFC 3986. It is the format HTML forms POST by default and the format URLSearchParams produces. It follows percent encoding with one infamous exception: a space becomes +, not %20, and consequently a literal plus sign must become %2B.

new URLSearchParams({ q: "coffee & cream 50%" }).toString()
// → "q=coffee+%26+cream+50%25"

Compare all three outputs for the same input side by side:

| Input: coffee & cream 50% | Output | |---|---| | encodeURI | coffee%20&%20cream%2050%25 | | encodeURIComponent | coffee%20%26%20cream%2050%25 | | URLSearchParams (form encoding) | coffee+%26+cream+50%25 |

Both of the last two are correct in a query string — servers decode + and %20 identically in the query component. The trap runs the other way: if a decoder applies form rules to a path, or a value containing a real + is decoded as form data, the plus silently becomes a space.

I hit this exact bug while wiring up an email-confirmation endpoint. I tested with my own Gmail alias, lilei+test@gmail.com, passed it through a link builder that used plain string concatenation instead of encodeURIComponent, and the confirmation handler received lilei test@gmail.com — the framework decoded the query with form rules and turned the un-escaped + into a space. The lookup failed for every user with a plus alias and no one else, which made it look intermittent for two days. Encoding the address as a component (lilei%2Btest%40gmail.com) fixed it in one line. Since then I sanity-check any suspicious string in Toolora's URL encoder/decoder, which shows component and full-URL modes side by side, before I blame the backend.

A worked example: building one URL correctly

Say you need to link to a search results page with two parameters: a query of C++ & Rust and a redirect path of /docs/intro.

Wrong (one encodeURI over the whole thing):

https://example.com/search?q=C++%20&%20Rust&next=/docs/intro

The + signs survive (they're in encodeURI's safe list) and later decode as spaces; the data & still splits the parameter.

Right (encode each component, then assemble):

const url = "https://example.com/search"
  + "?q="    + encodeURIComponent("C++ & Rust")
  + "&next=" + encodeURIComponent("/docs/intro");
// → https://example.com/search?q=C%2B%2B%20%26%20Rust&next=%2Fdocs%2Fintro

Every structural character in the final URL was typed by you; every character that came from data is escaped. When I need to verify a URL like this or take apart one somebody else built, I paste it into the URL query string parser and builder, which decodes each parameter into a table so a smuggled & or a plus-as-space problem is visible immediately. For inspecting the rest of the URL's anatomy — scheme, host, path, fragment — the URL parser does the same job for the whole string.

The decision rule

Four situations, four answers:

  1. Inserting a value into a URL you are building (query value, path segment, fragment): encodeURIComponent. This is 95% of cases.
  2. Serializing a whole set of form fields or query parameters: URLSearchParams (form encoding). It handles the joining and escaping for you and its +-for-space output is valid in query strings.
  3. Transmitting a complete URL you received and must not restructure: encodeURI, the only case it exists for.
  4. Writing a URL into an HTML attribute: percent-encode the components first, then HTML-escape the result (&&) — two different encodings layered, not one. Toolora's HTML entities encoder covers that second layer.

And one prohibition: never call encodeURIComponent twice on the same string. %26 becomes %2526, and double-decoding bugs are even harder to spot than the single-encoding ones.

Percent encoding is one mechanism with three policies on top. Pick the policy by asking what the string is — a value, a parameter set, or a finished URL — and the right function picks itself.


Made by Toolora · Updated 2026-07-02