URL Encoding: Percent-Encode Query Parameters and Paths

Percent-encoding replaces unsafe characters with %XX so arbitrary text is safe in a URL. Know which characters to escape and how to decode them back.

What Percent-Encoding Is

Per RFC 3986, characters that are not allowed in a URL are replaced with a percent sign and two hex digits. A space becomes %20, an ampersand becomes %26, a # becomes %23. The hex pair is the UTF-8 byte value of the character; characters outside ASCII produce multiple %XX pairs in sequence.

The URL Decode tool handles the round trip — paste an encoded URL and see the decoded form, or vice versa.

Reserved vs Unreserved Characters

RFC 3986 splits URL characters into three groups. The distinction matters because reserved characters carry meaning in URL syntax (slash separates path segments, ? starts the query, & separates query pairs):

Group Characters Encoding rule
Unreserved A–Z a–z 0–9 - . _ ~ Never encoded
Reserved — gen-delims : / ? # [ ] @ Encoded inside a value; left alone as URL structure
Reserved — sub-delims ! $ & ' ( ) * + , ; = Encoded inside a value; some need encoding in some contexts
All other characters (spaces, accented letters, emoji, control chars) Always encoded

The "inside a value vs as structure" distinction is what makes encodeURIComponent vs encodeURI matter.

encodeURIComponent vs encodeURI

Wrong tool for the job here is the most common URL-encoding bug:

// One value going into a query parameter or path segment
encodeURIComponent('a/b?c=d&e')
// → "a%2Fb%3Fc%3Dd%26e"   — / ? = & are all escaped, the value can't break out

// A whole URL where : / ? # & = are structural
encodeURI('https://example.com/path?q=hello world')
// → "https://example.com/path?q=hello%20world"
// → only the space is escaped; the structural characters survive

Rule of thumb: encodeURIComponent for a single field, encodeURI for a whole URL. Mixing them is the bug behind half the "double-encoded URL" reports.

The Plus vs %20 Trap

In query strings a space can appear two ways:

  • %20 — the standard percent-encoded form (per RFC 3986)
  • + — the legacy form encoding used by HTML form submissions (application/x-www-form-urlencoded)

decodeURIComponent does not turn + into a space — that's only URLSearchParams and form-decoders. If you're parsing a form-encoded value by hand, replace + with space first:

const formValue = decodeURIComponent(raw.replace(/\+/g, ' '));

Or just use URLSearchParams, which handles both correctly.

Double-Encoding

Encoding an already-encoded string turns %20 into %2520 (the % itself gets encoded as %25). The detection rule: if you see %25 followed by two hex digits, the value was encoded twice.

encodeURIComponent('a b')           // "a%20b"  — once
encodeURIComponent('a%20b')         // "a%2520b" — twice (the % got encoded)
decodeURIComponent('a%2520b')       // "a%20b"  — back to once
decodeURIComponent('a%20b')         // "a b"    — back to the original

If you don't know how many times it was encoded, decodeURIComponent in a loop until the result stops changing:

let value = raw;
while (decodeURIComponent(value) !== value) {
  value = decodeURIComponent(value);
}

Building Safe Query Strings

Prefer URLSearchParams (browser and Node stdlib) over string concatenation. It encodes keys and values correctly, handles multi-value parameters, and decodes + as space the way form data expects:

const params = new URLSearchParams();
params.set('q',      'hello world');
params.set('filter', 'name:Ada & year:1815');
params.append('tag', 'dev');
params.append('tag', 'pioneer');

const url = `https://example.com/search?${params}`;
// → https://example.com/search?q=hello+world&filter=name%3AAda+%26+year%3A1815&tag=dev&tag=pioneer

URLSearchParams uses + for spaces (form-encoding flavor), not %20 — fine for query strings that real servers consume; choose encodeURIComponent directly if you specifically need %20.

Common Pitfalls

The recurring URL-encoding bugs, in rough order of frequency:

Pitfall Fix
Using encodeURI on a single value (leaves ? and & unescaped) Use encodeURIComponent for values
Using encodeURIComponent on a whole URL (breaks :// and the query separator) Use encodeURI for whole URLs, or build with URLSearchParams
+ not decoded as space in query strings Use URLSearchParams, or .replace(/\+/g, ' ') first
Double-encoded values silently passed through Detect %25XX and decode in a loop
Unicode characters encoded with the wrong byte width encodeURIComponent uses UTF-8; legacy code may use UTF-16 or Latin-1

See also

For embedding JSON inside a URL parameter (a common cross-format pitfall), see Escape JSON as a string literal — JSON has to be stringified, then URL-encoded, both layers required.

Sources

Last reviewed June 2026.