URL Encoding Explained: encodeURIComponent, Percent-Encoding, and Query String Pitfalls
The developer's practical guide to encodeURIComponent vs URLSearchParams, why + and %20 are not always interchangeable, and the three query-string bugs that catch even experienced engineers off guard.
URL Encoding Explained: encodeURIComponent, Percent-Encoding, and Query String Pitfalls
The web runs on URLs, and URLs have rules. A handful of characters — & = ? # : / — carry structural meaning: they split parameters apart, mark the start of a fragment, separate host from path. When your data contains any of those characters, you must encode them or the parser misreads them as structure.
That encoding is called percent-encoding: each byte of the character's UTF-8 representation is written in hexadecimal and prefixed with %. A space becomes %20. An ampersand becomes %26. The Thai character ท (U+0E17) becomes %E0%B8%97 — three UTF-8 bytes, three %xx triplets.
JavaScript gives you two built-in entry points — encodeURIComponent and encodeURI — plus a higher-level alternative in URLSearchParams. Knowing exactly which to reach for, and when they disagree, prevents an entire class of subtle production bugs.
What encodeURIComponent Actually Escapes
encodeURIComponent leaves exactly 66 characters untouched: A–Z (26), a–z (26), 0–9 (10), and the four unreserved marks -_.~. Everything else — including all 18 of RFC 3986's reserved delimiter characters (: / ? # [ ] @ ! $ & ' ( ) * + , ; =) — is percent-encoded (RFC 3986 §2.3).
That is why it is the right function for a single query-parameter value. You want & and = inside a value to appear as %26 and %3D, not interpreted as parameter delimiters by the server.
encodeURI, by contrast, is for a complete URL. It skips all the structural delimiters because in a whole URL those characters are doing their structural job. Use encodeURIComponent on a full URL and you get a broken link:
Input: https://example.com/search?q=test
Output: https%3A%2F%2Fexample.com%2Fsearch%3Fq%3Dtest
The colons and slashes are gone. A browser cannot navigate to that string. Rule of thumb: encodeURI for whole URLs, encodeURIComponent for one value you are interpolating into a query string.
The + vs %20 Divergence
Here is a real input/output pair that trips people up in production:
const city = "São Paulo"
// encodeURIComponent — space becomes %20
encodeURIComponent(city)
// → "S%C3%A3o%20Paulo"
// URLSearchParams — space becomes +
new URLSearchParams({ city }).toString()
// → "city=S%C3%A3o+Paulo"
Both are syntactically valid. The + convention comes from the application/x-www-form-urlencoded specification — the format HTML forms use when submitting data — where + and %20 both represent a space. Most server-side decoders handle them identically: PHP's $_GET, Ruby's Rack, Python's urllib.parse.parse_qs with default settings all convert + to a space.
The trap is decoders that call unquote instead of unquote_plus. Python's bare urllib.parse.unquote does not convert + to a space. I spent an afternoon tracing this exact mismatch between a JavaScript frontend using URLSearchParams and a Python webhook handler calling urllib.parse.unquote. Every event name containing a space showed up with a stray + in our analytics dashboard. The fix was a single word — replacing unquote with unquote_plus — but finding it required knowing that the +/%20 split exists at all, and that different corners of the ecosystem handle it differently.
To avoid the ambiguity: use encodeURIComponent for individual values when precision matters. It always emits %20 for spaces, which every decoder — unquote, unquote_plus, and everything else — correctly handles.
Building Multi-Parameter URLs Safely
When you have more than one parameter, assemble the query string with URLSearchParams or the URL constructor rather than string concatenation. Both handle encoding automatically and prevent the common mistake of forgetting to encode a single parameter:
// URLSearchParams — compact, handles repeated keys
const params = new URLSearchParams({
q: "São Paulo weather",
lang: "pt-BR"
})
`https://api.example.com/data?${params}`
// → "https://api.example.com/data?q=S%C3%A3o+Paulo+weather&lang=pt-BR"
// URL constructor — most explicit, fewest surprises
const url = new URL("https://api.example.com/data")
url.searchParams.set("q", "São Paulo weather")
url.searchParams.set("lang", "pt-BR")
url.toString()
// → "https://api.example.com/data?q=S%C3%A3o+Paulo+weather&lang=pt-BR"
If %20 matters for your downstream parser (rather than +), use encodeURIComponent per value and interpolate manually:
const q = encodeURIComponent("São Paulo weather")
const lang = encodeURIComponent("pt-BR")
`https://api.example.com/data?q=${q}&lang=${lang}`
// → "https://api.example.com/data?q=S%C3%A3o%20Paulo%20weather&lang=pt-BR"
When debugging an unfamiliar URL or rebuilding a query string from its parts without writing code, the URL Query String Parser & Builder splits any URL into a decoded key/value table, lets you edit each parameter, and rebuilds a correctly-encoded query string instantly. The URL Encoder / Decoder handles single-value encoding in all three modes — component, full URL, and form-data — and flags the output mode so you can see exactly which encoding style produced the result.
Three Query String Pitfalls That Catch Everyone Eventually
Pitfall 1 — Double encoding. This is the nastiest bug in this space, because the result still looks like a valid encoded string. It happens when a value that already contains %xx sequences gets encoded a second time. The % character itself becomes %25, so %20 (a space) becomes %2520, and %26 becomes %2526. The tell: spot %25 where you did not put one. If you see %2520 in a URL you built, the value was encoded twice. Decode exactly once, or — better — encode exactly once, at the last moment before the URL is assembled, and never re-encode a value that already contains percent-sequences.
Pitfall 2 — Encoding the wrong scope. Calling encodeURIComponent on an entire URL is the most common beginner mistake. It escapes the :// and all path slashes, producing a string that looks like a URL but cannot be navigated to. If you are passing a whole URL as the value of another parameter — an OAuth redirect_uri, a tracking next parameter — encode only the inner URL as a value (using component mode), then embed it inside the outer URL's query string. The outer structure stays intact; the inner URL's delimiters get escaped.
Pitfall 3 — The literal plus sign. In any query string, + means a space. If your data contains an actual plus sign — a phone number in E.164 format like +1-415-555-0100, a math expression, or a chemical formula — a raw + in a query parameter will arrive at the server as a space character. encodeURIComponent escapes it to %2B automatically. URLSearchParams does too. The danger is building a query string by hand and forgetting to escape it: writing ?phone=+1-415 by hand (not via an encoder) sends 1-415 to the server.
Practical Decision Checklist
- Single value, string interpolation →
encodeURIComponent - Multiple parameters at once →
URLSearchParamsorURL.searchParams.set() - Full URL as a parameter value (OAuth
redirect_uri, nested link) →encodeURIComponenton the inner URL - Decoding a query string in Python →
urllib.parse.parse_qsorunquote_plus, not bareunquote - Seeing
%25in output you did not create → double encoding, decode once - Need to preserve a literal
+sign → always run the value through an encoder; never embed raw+into a query string by hand
Percent-encoding is a mechanical transformation with a small surface area. The complexity is really two questions: which characters need escaping for the scope you are working in (a value, a full URL, a form body), and which space encoding — + or %20 — your server-side decoder expects. Get those two things right and query string bugs become immediately obvious rather than silently corrupting data.
Made by Toolora · Updated 2026-06-29