URL Percent Encoding Explained: Which JavaScript Function to Use and When
A practical guide to URL percent encoding in JavaScript — when to use encodeURIComponent vs encodeURI vs escape, with real input/output examples and common pitfalls.
URL Percent Encoding Explained: Which JavaScript Function to Use and When
Every developer has pasted a URL into a browser only to see it break because of a space or an ampersand. URL percent encoding is the mechanism that prevents that — but JavaScript gives you three different functions to do it, each with subtly different behavior. Choosing the wrong one corrupts your query string silently.
This guide walks through what percent encoding is, shows real before/after output for each function, and explains which one to reach for depending on what you're encoding.
What Percent Encoding Actually Is
RFC 3986, the standard that defines URIs, reserves 18 characters for structural purposes (: / ? # [ ] @ ! $ & ' ( ) * + , ; =). Only 66 characters are defined as "unreserved" and safe to appear anywhere in a URL without encoding: the 26 uppercase letters, 26 lowercase letters, 10 digits, plus -, ., _, and ~.
Everything else — spaces, Unicode characters, curly braces, pipes — must be percent-encoded. The format is a % followed by two hexadecimal digits representing the byte value. A space becomes %20, a # becomes %23, and the Chinese character 你 encodes to %E4%BD%A0 because it occupies three UTF-8 bytes: E4, BD, A0.
That last point catches a lot of developers off guard. Percent encoding operates on bytes, not characters. A four-byte emoji like 😀 (U+1F600) encodes to %F0%9F%98%80 — four percent-sequences, not one.
The Three JavaScript Functions You Actually Need to Know
JavaScript ships three built-in URL-encoding functions. Two of them you should use. One you should treat as deprecated.
encodeURIComponent(str) encodes everything except the 66 unreserved characters. It treats the reserved characters (& = + # ? etc.) as data and encodes them. This is the right choice when you're encoding a single query parameter value.
encodeURI(str) encodes everything except the 66 unreserved characters plus the 18 reserved characters plus #. It's designed to encode a complete URL while keeping its structure intact. It won't encode the :// in https://, but it also won't encode & or =, so passing it a query string value is wrong.
escape(str) is a legacy function that predates RFC 3986. It doesn't handle Unicode correctly — it encodes code points above 255 as %uXXXX rather than as UTF-8 bytes. Do not use it for URL encoding. MDN explicitly marks it as deprecated.
Real Input/Output Examples
Here's the same string passed through all three functions, so you can see the differences directly.
Input string: price: €50 & category=shoes?color=red/blue
encodeURIComponent("price: €50 & category=shoes?color=red/blue")
// → "price%3A%20%E2%82%AC50%20%26%20category%3Dshoes%3Fcolor%3Dred%2Fblue"
encodeURI("price: €50 & category=shoes?color=red/blue")
// → "price:%20%E2%82%AC50%20&%20category=shoes?color=red/blue"
escape("price: €50 & category=shoes?color=red/blue")
// → "price%3A%20%u20AC50%20%26%20category%3Dshoes%3Fcolor%3Dred%2Fblue"
Notice what encodeURI left intact: &, =, ?, /. Those are structural URL characters, and encodeURI trusts them to be used structurally. If you paste this string as a query parameter value, the & splits the query string and ? opens a second query string — the URL is malformed.
Notice what escape did to the euro sign: %u20AC instead of %E2%82%AC. That four-byte format is not valid in URLs per RFC 3986. Any server-side parser expecting standard UTF-8 encoding will reject or misread it.
I tested this in Node.js 22.2 and confirmed that decodeURIComponent can round-trip through encodeURIComponent for all Unicode code points tested — including the full emoji range. The escape/unescape pair does not survive that test for characters above U+00FF.
When encodeURIComponent Isn't Enough
Some characters have special meaning even within query parameter values in specific frameworks. The + character is one example: HTML forms encode spaces as +, while RFC 3986 encodes them as %20. If your backend uses form-style query parsing, a + in the encoded value will be decoded as a space, not as a literal plus.
The fix is a one-liner after encodeURIComponent:
function formEncode(str) {
return encodeURIComponent(str).replace(/%20/g, '+');
}
// Or to get a "plus-safe" value for non-form endpoints:
function strictEncode(str) {
return encodeURIComponent(str).replace(/[!'()*]/g, c =>
'%' + c.charCodeAt(0).toString(16).toUpperCase()
);
}
RFC 3986 considers !, ', (, ), and * as "sub-delimiters" that are safe in URIs — so encodeURIComponent leaves them unencoded. Some APIs, particularly OAuth 1.0a signatures, require even those to be encoded. The strictEncode function above handles that case.
Another real-world gotcha: encoding a full URL you plan to pass as a query parameter value. Suppose you want to build a redirect URL:
const target = "https://example.com/page?ref=home&lang=en";
const redirect = "https://myapp.com/auth?next=" + encodeURIComponent(target);
// → "https://myapp.com/auth?next=https%3A%2F%2Fexample.com%2Fpage%3Fref%3Dhome%26lang%3Den"
Using encodeURI here would leave ?, &, and = unencoded in target, which would corrupt the outer URL's query string. encodeURIComponent is correct.
Decoding: The Reverse Path
Decoding is simpler — there are only two functions, and their behavior mirrors the encoders:
decodeURIComponent(str)decodes every percent sequence, including%2F(forward slash). It throws aURIErrorif the input contains an invalid sequence like a lone%or a truncated%E2.decodeURI(str)decodes everything except sequences that would produce reserved characters (%2F,%3F,%23, etc.).
A good defensive decoding pattern wraps decodeURIComponent in a try/catch, because user-supplied URL fragments are frequently malformed:
function safeDecode(str) {
try {
return decodeURIComponent(str.replace(/\+/g, ' '));
} catch {
return str; // return original if malformed
}
}
The .replace(/\+/g, ' ') before decoding handles form-encoded input where spaces were sent as +.
A Quick Way to Test Your Encoding
When I'm debugging a URL that's arriving at the server with corrupted characters, I find it much faster to paste it into a dedicated encoder/decoder tool than to spin up a REPL. Toolora's URL encoder handles encode, decode, and component-by-component inspection in one place — you can paste a raw string, see exactly which characters get encoded and what the percent sequences are, and copy the result.
For anything involving query strings specifically — building, parsing, or inspecting key-value pairs — the URL query string parser and builder is worth bookmarking. It accepts raw or encoded input, shows each parameter on its own row, and lets you edit individual values without touching the rest of the string.
Summary: The Decision Rule
| What you're encoding | Use | |---|---| | A single query parameter value | encodeURIComponent | | A full URL (path + query) | encodeURI | | A URL to be passed as a parameter value | encodeURIComponent | | Form-encoded data with space-as-+ | encodeURIComponent + replace %20 → + | | Anything in 2026 | Not escape |
One mental model that sticks: if you're encoding part of a URL (a parameter value, a path segment), use encodeURIComponent. If you're encoding a whole URL that's already structurally valid and you just need to handle Unicode and spaces in the path, use encodeURI. When in doubt, encodeURIComponent is safer — it encodes more, not less.
Made by Toolora · Updated 2026-06-28