URLs can only safely carry a small set of ASCII characters, yet we routinely stuff names, emails, search terms, and JSON into them. Percent-encoding is the mechanism that bridges that gap, and getting it wrong produces some of the most persistent, hard-to-spot bugs in web development.
What percent-encoding actually is
A URL is defined by RFC 3986, and that spec only permits a limited alphabet of characters to appear directly in a URL. Anything outside that set, or any character that has a special structural meaning in the wrong place, must be escaped.
Percent-encoding works on bytes, not characters. The rule is simple:
- Take the character and encode it as one or more bytes using UTF-8.
- Replace each byte with a followed by its two-digit uppercase hexadecimal value.text
%
A space (byte
0x20%200x26%26é0xC3 0xA9%C3%A9textspace -> %20 & -> %26 é -> %C3%A9
This byte-level detail matters: percent-encoding is not "replace weird characters with codes," it is "serialize to UTF-8, then escape the bytes." Any encoder that doesn't go through UTF-8 first will mangle non-ASCII input.
Reserved vs unreserved characters
RFC 3986 splits characters into two groups that decide what needs escaping.
Unreserved characters
These are always safe and never need encoding:
textA-Z a-z 0-9 - _ . ~
That's the entire unreserved set. Letters, digits, hyphen, underscore, period, and tilde. A correct encoder leaves these untouched. (Some older encoders escaped
~Reserved characters
Reserved characters have structural meaning in a URL. They are the delimiters that separate one part of the URL from another:
text: / ? # [ ] @ (general delimiters) ! $ & ' ( ) * + , ; = (sub-delimiters)
The key insight is that reserved characters are only special in context. A
/%2F?#&=This is why "do I need to encode this character?" has no universal answer. It depends entirely on where the character is going and whether it's acting as a delimiter or as literal data.
encodeURI vs encodeURIComponent
JavaScript ships two global functions, and the difference between them is the single most common source of URL bugs.
encodeURI: for a whole URL
encodeURIjsencodeURI('https://example.com/search?q=a b&x=1'); // 'https://example.com/search?q=a%20b&x=1'
Notice that
:/?&=encodeURIencodeURIComponent: for one piece
encodeURIComponentjsencodeURIComponent('a b&x=1'); // 'a%20b%26x%3D1'
Here
&%26=%3DThe rule of thumb
- Building a URL from parts? Encode each part with , then assemble.text
encodeURIComponent - Already have a full URL and just want to clean it up? (rarely needed in practice).text
encodeURI
Never use
encodeURI&encodeURIBoth functions deliberately leave a few characters unescaped that
encodeURIComponent!'()*jsfunction strictEncode(str) { return encodeURIComponent(str).replace( /[!'()*]/g, c => '%' + c.charCodeAt(0).toString(16).toUpperCase() ); }
Encoding query parameters correctly
Query strings are where encoding goes wrong most often, because both the keys and the values can contain reserved characters.
The wrong way is to concatenate raw strings:
js// BROKEN: breaks if name or note contains & = # or space const url = `/save?name=${name}¬e=${note}`;
If
notecost = $5 & up=&Encode each key and value independently:
jsconst url = `/save?name=${encodeURIComponent(name)}` + `¬e=${encodeURIComponent(note)}`;
Better still, let
URLSearchParamsjsconst params = new URLSearchParams({ name: name, note: 'cost = $5 & up', }); const url = `/save?${params.toString()}`; // /save?name=...¬e=cost+%3D+%245+%26+up
The
URLjsconst u = new URL('https://example.com/save'); u.searchParams.set('q', 'rock & roll'); u.toString(); // https://example.com/save?q=rock+%26+roll
Using these built-ins is almost always safer than hand-rolling string concatenation, and it sidesteps the next problem entirely.
The space and plus-sign trap
Here is the bug that surprises nearly everyone. There are two conventions for encoding a space in a URL, and they live in different parts of the URL.
- In the path, a space is . Atext
%20is a literal plus.text+ - In a query string using (the format browsers use for form submissions), a space istext
application/x-www-form-urlencoded, and a literal plus istext+.text%2B
This split exists for historical reasons: HTML form encoding predates and diverges from the generic URL spec, and that legacy convention is baked into how servers parse query strings.
The practical consequences:
- encodes spaces astext
URLSearchParams(form-encoding rules). Sotext+givestextnew URLSearchParams({q: 'a b'}).toString().textq=a+b - encodes spaces astext
encodeURIComponentand atext%20astext+.text%2B - Both andtext
a+bdecode totexta%20bon a well-behaved server reading a query string, but a server reading a path will treattexta bas a literal plus.text+
The classic failure: a user searches for
C++encodeURIComponentC%2B%2BC++C The reverse failure: you store a value like
2 + 2%20+2 + 22 2How to stay safe
- Pick one tool for a given context and let it own both encoding and decoding. If you build query strings with , parse them withtext
URLSearchParamstoo, and thetextURLSearchParams/space round-trip stays consistent.text+ - Never mix: don't encode with and decode by manually replacingtext
encodeURIComponentwith a space, or vice versa.text+ - When debugging, remember that a in a URL is ambiguous on sight. You cannot tell whether it means "space" or "literal plus" without knowing which part of the URL it's in and which convention produced it.text
+
If you ever need to eyeball what a particular string encodes to, or decode a captured URL to see what's really in it, our URL encoder and decoder runs entirely in your browser so nothing you paste leaves your machine. It's a quick way to confirm whether that mysterious
%2BA short checklist
When you're encoding a URL, run through this:
- Am I encoding a whole URL or a piece of one? Whole URL is rare; you almost always want to encode pieces with and assemble them.text
encodeURIComponent - Are reserved characters acting as structure or data? If ,text
&,text=,text/, ortext?are data inside a value, they must be escaped.text# - Path or query? Decide before you pick how spaces are handled. In doubt, is safe in both contexts.text
%20 - Same tool in and out. Encode and decode with matching conventions so andtext
+round-trip correctly.text%2B - Prefer the built-ins. and thetext
URLSearchParamsAPI eliminate most hand-encoding mistakes.textURL
Percent-encoding looks fiddly, but it reduces to one idea: characters that have meaning to the URL must be escaped when used as data, and there are two slightly different conventions for spaces depending on where you are. Internalize those two facts, lean on
encodeURIComponentURLSearchParams