URL Encoding (Percent-Encoding) Explained
URLs permit fewer characters than most people assume. Everything else has to be written as a %XX hex escape — that's percent-encoding. The catch is that which characters need escaping depends on where in the URL the value goes, which is exactly why JavaScript ships four different functions for it.
Reserved versus unreserved characters
RFC 3986 splits URL characters into two groups. Unreserved characters — A–Z, a–z, 0–9, and the four marks - _ . ~ — are always safe as-is, and encoding them can actually make two identical URLs compare as different. Reserved characters — : / ? # [ ] @ ! $ & ' ( ) * + , ; = — act as structural delimiters.
The rule that matters: leave a reserved character alone when it is acting as a delimiter, and encode it when it is data. The & that separates query parameters stays literal, but an & inside a value must become %26 — otherwise the parser truncates your parameter right there.
How the four JavaScript functions differ
encodeURI assumes you're encoding a whole URL, so it leaves delimiters (: / ? # & =) untouched. encodeURIComponent assumes you're encoding a single value, so it escapes delimiters too. In practice, what you want is almost always encodeURIComponent.
const value = "a&b=c/d?e"; encodeURI(value) // "a&b=c/d?e" ← unchanged, unsafe as a value encodeURIComponent(value) // "a%26b%3Dc%2Fd%3Fe" ← safe // correct assembly const url = "/search?q=" + encodeURIComponent(value);
| Function | Leaves unescaped | Use for |
|---|---|---|
| encodeURIComponent | A-Z a-z 0-9 - _ . ! ~ * ' ( ) | Query parameter values, one path segment |
| encodeURI | The above plus : / ? # [ ] @ & = + $ , | An already-assembled full URL |
| escape (deprecated) | Mangles Unicode into %uXXXX | Never |
| encodeURIComponent + fixup | Also escapes ! ' ( ) * | Strict RFC 3986 conformance |
How to use it
- Paste the string to encode, or the %XX string to decode.
- Choose the Encode or Decode direction.
- If you're converting a single value, the component style — escaping every reserved character — is usually correct.
- Copy the result into your URL.
Is a space %20 or +?
Both exist, in different contexts. %20 is the canonical RFC 3986 form and is valid anywhere in a URL. The + is a convention from HTML form submission (application/x-www-form-urlencoded) and only means 'space' inside a query string.
Paths are where this bites. A + in a path is a literal plus sign, so /files/my+file.pdf refers to a file whose name contains a plus. Conversely, because servers turn + into a space when parsing query parameters, a value that must contain a real plus sign has to be encoded as %2B. This is the classic bug when passing phone numbers like +1... or Base64 strings as parameters.
Non-ASCII text and emoji
Percent-encoding operates on bytes, so characters are first converted to UTF-8 bytes and each byte is written as %XX. A Korean syllable is 3 bytes in UTF-8, so '한' becomes %ED%95%9C — three escapes for one character. Emoji are 4 bytes and produce four.
Browsers display such URLs in readable form in the address bar, but copying one often yields the encoded version. They are the same URL; the encoded form is what actually reaches the server.
Frequent mistakes
- Double encoding: escaping an already-escaped string turns % into %25, producing things like %2520. If you see %25 after a decode, suspect double encoding.
- Running encodeURIComponent over a whole URL: https:// becomes https%3A%2F%2F and the link breaks.
- Leaving a literal # in a query value: the server never receives anything after it, because fragments stay client-side.
- Storing the encoded form in a database: store the original and encode at output time.
- Encoding unreserved characters needlessly: %41 means the same as A, but cache keys and signature checks may treat the two strings as different.
Frequently Asked Questions
- Should I use encodeURI or encodeURIComponent?
- If you're inserting a single value into a URL, use encodeURIComponent. Search terms, IDs, and redirect targets are all values. encodeURI is only for tidying an already-complete URL string, which is rarely what you need.
- How do I pass a redirect URL as a parameter?
- Wrap it in encodeURIComponent. Written raw as ?next=https://example.com/a?b=c, the server cannot tell whether the second ? starts a new parameter or belongs to the inner URL; as ?next=https%3A%2F%2Fexample.com%2Fa%3Fb%3Dc it is unambiguous. Separately, always validate a received redirect target against an allowlist — redirecting without validation is an open-redirect vulnerability.
- Why are some URLs full of things like %EC%95%88?
- Those are non-ASCII characters percent-encoded byte by byte as UTF-8. Because one Korean or Chinese character is 3 bytes, it expands to three %XX escapes, which is why such URLs look so long. Browsers usually render them back as readable characters in the address bar.
- Why does decoding sometimes throw an error?
- A % that isn't followed by two hex digits is malformed. Trying to decode a string like '100% complete' fails at the '% ' sequence. Check whether you're attempting to decode text that was never encoded in the first place.
- Is hand-assembling query strings a good idea?
- No. Both browsers and Node.js provide URLSearchParams, which handles the escaping for you and leaves less room for error: new URLSearchParams({ q: 'search term&' }).toString(). Just remember that URLSearchParams encodes spaces as +.
- Is there a length limit for encoded URLs?
- Not in the standard, but there are practical ceilings. Roughly 2,000 characters is broadly safe across browsers and servers, and some servers reject headers beyond 8KB. Since encoding can triple length, long payloads belong in a POST body rather than a URL.
💡 Note: If you see %25 in your output, it's almost certainly double encoding. Decode once more and check.