A URL can only contain a small, safe set of characters — but the data you want to put in one (search queries, file names, redirect targets) contains everything else. Percent-encoding is the bridge: every unsafe byte becomes % followed by two hex digits. This is the reference for doing it correctly: which characters must be encoded, when, and in which of the two modes. To encode or decode something right now, use our URL encoder / decoder.
Why encoding exists
A URL is parsed by structure: ? starts the query, & separates parameters, # starts the fragment, / separates path segments. If your data itself contains those characters, the parser can’t tell structure from content. Searching for the string fish & chips? inside ?q=fish & chips? breaks into nonsense. Encoding the data — ?q=fish%20%26%20chips%3F — removes the ambiguity: structural characters appear literally, data characters appear encoded, and the two never collide.
Three classes of character matter:
- Unreserved —
A–Z a–z 0–9 - _ . ~— never need encoding, anywhere, ever. - Reserved —
: / ? # [ ] @ ! $ & ' ( ) * + , ; =— the structural set. Encode them when they appear as data; leave them when they’re doing their structural job. - Everything else — spaces, quotes,
< >,%itself, and every non-ASCII character — must always be encoded.
The reference table
The characters you will actually meet, as data:
| Character | Encoded | Character | Encoded |
|---|---|---|---|
| space | %20 (or + in query strings — see below) | & | %26 |
% | %25 | = | %3D |
? | %3F | # | %23 |
/ | %2F | + | %2B |
: | %3A | @ | %40 |
" | %22 | < / > | %3C / %3E |
| newline | %0A | tab | %09 |
Non-ASCII characters are encoded byte-by-byte as UTF-8: é is the two bytes C3 A9, so it becomes %C3%A9; the character 猫 is %E7%8C%AB. If you ever see a single %E9 for é, you’re looking at legacy Latin-1 encoding — one of the classic mojibake recipes.
The two modes: component vs full URL
This is where most bugs live. There are two legitimate things to encode, and they are not interchangeable:
- Component mode (JavaScript’s
encodeURIComponent) encodes a value you’re inserting into a URL: a query parameter, a path segment, a search term. Reserved characters in the value are encoded — including/,?and&— because inside a value they are data, not structure. - Full-URL mode (
encodeURI) encodes a whole URL that already has its structure: it leaves: / ? & = #intact and only encodes the characters that are illegal anywhere (spaces, quotes, non-ASCII).
Use the wrong one and you get one of two failures: encoding a whole URL in component mode produces %3A%2F%2F soup that no server recognises; encoding a value in full-URL mode lets a stray & in someone’s search term silently split your parameter in two. Our encoder offers both modes side by side for exactly this reason.
The + vs %20 question
In the query string only, a space may also be written as +. This is a fossil of application/x-www-form-urlencoded — the format HTML forms submit — where spaces become + and a literal plus sign must therefore be encoded as %2B. Rules of thumb:
- In a path segment, space is
%20; a+there means a literal plus. - In a query string,
+means space (when form-decoded);%20also means space and always works. - A literal
+in data must be%2Bin both places.
Decoders that ignore this distinction are why “C++” keeps arriving at servers as “C ”.
Worked example
Build a search URL for the query tomato & basil "fresh", step by step:
- Raw value:
tomato & basil "fresh" - Component-encode it:
tomato%20%26%20basil%20%22fresh%22 - Assemble:
https://example.com/search?q=tomato%20%26%20basil%20%22fresh%22&page=1
The structure (?, &, =) stayed literal because we encoded the value, then built the URL around it — the order of operations that prevents almost every encoding bug.
Decoding errors to recognise
- A lone or malformed
%—%zz, or a trailing%2: the%wasn’t itself encoded (%25) when it should have been. - Double encoding —
%2520: the value was encoded twice, so one decode yields%20instead of a space. Common when a framework encodes what you already encoded. - Invalid UTF-8 after decoding —
%C3%28: the bytes don’t form legal UTF-8, usually Latin-1 data wearing a UTF-8 costume. Our decoder locates these precisely; a precise position is worth more than a generic “malformed URI” exception, which is all JavaScript’sdecodeURIComponentwill tell you.