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
- RFC 3986 — Uniform Resource Identifier (URI): Generic Syntax (the percent-encoding spec)
- MDN —
encodeURIComponent - MDN —
encodeURI - MDN —
URLSearchParams
Last reviewed June 2026.
JSON repair guides
Topic hubs
- JSON Parse Errors: Read the Message, Jump to the Fix
- Fix Invalid JSON: From 'What's Wrong' to a Clean File
- JSON Formatter, Validator, Viewer: Pick the Right Tool
- Repair LLM JSON Output: Handling Almost-JSON from AI
- Privacy: JSON Tools That Don't Leave Your Browser
- JSON Interop: YAML, CSV, XML, JWT, Schema
Specific guides
- How to Decode Base64 Strings (and JWT Payloads)
- Convert YAML to JSON (and Avoid Indentation Errors)
- Convert JSON to CSV: Flatten an Array of Objects
- Convert JSON to XML: Root Elements, Attributes, and Arrays
- Escape JSON as a String Literal (and Decode Double-Encoded JSON)
- Fix Trailing Comma in JSON
- Fix Single Quotes in JSON
- Fix Unquoted Keys in JSON
- Repair LLM JSON Output
- Fix JSON Parse Error: Expected Property Name
- JSON vs JS Object Literal: The Key Differences
- Validate JSON Before API Requests
- JSON Formatter vs JSON Repair
- Fix JSON Unexpected Token Errors
- JSON to JavaScript Object Converter