A search query with an ampersand in it truncates. A redirect URL passed as a parameter loses everything after its own question mark. A filename with a hash sign becomes a fragment. All three are the same bug, and all three come from not encoding a value before putting it inside a URL.
Reserved, Unreserved, and Everything Else
RFC 3986 splits characters into groups, and the split is the whole basis of the rules.
Unreserved characters never need encoding: A-Z, a-z, 0-9, and the four punctuation marks `-`, `.`, `_`, `~`. These mean the same thing everywhere in a URL.
Reserved characters have structural jobs. `?` starts the query string. `#` starts the fragment. `&` separates parameters. `=` separates a key from its value. `/` separates path segments. `:` separates the scheme. When you want one of these as data rather than structure, it must be percent-encoded.
Everything else, including spaces, non-ASCII letters and most symbols, has to be encoded because it either has no defined meaning or is unsafe in transit.
Percent-encoding replaces a character with `%` followed by its byte value in hexadecimal. A space becomes `%20`. An ampersand becomes `%26`. Characters outside ASCII are encoded as their UTF-8 bytes, so `é` becomes `%C3%A9`, two bytes and therefore two escapes.
Characters You Will Encode Most Often
The right column is what happens if you leave the character raw inside a query parameter.
| Character | Encoded | What breaks if you skip it |
|---|---|---|
| space | %20 | Truncation, or conversion to + depending on the parser |
| & | %26 | The value is split; everything after it becomes a new parameter |
| ? | %3F | Interpreted as the start of a nested query string |
| # | %23 | Everything after it is treated as a fragment and never sent to the server |
| = | %3D | Ambiguous key/value boundary; some parsers keep only the first part |
| / | %2F | Read as a path separator; breaks slugs and encoded paths |
| + | %2B | Decoded as a space by form parsers; a classic email address bug |
| % | %25 | The next two characters are read as a hex escape |
Why JavaScript Has Two Functions
`encodeURI` and `encodeURIComponent` are not alternatives. They encode different amounts on purpose, and picking the wrong one is the most common URL bug in JavaScript.
`encodeURI` is for a complete URL. It leaves reserved characters alone, because in a whole URL those characters are doing their structural job. Running it on `https://example.com/search?q=hello world` gives you a working URL with the space fixed and the `?` and `=` intact.
`encodeURIComponent` is for a single value going into a URL. It encodes reserved characters too, because inside a parameter they are data rather than structure. This is the one you want when building a query string.
The rule that covers almost every case: use `encodeURIComponent` on each key and each value, then join them yourself with `&` and `=`. Reach for `encodeURI` only when you have a URL that is already assembled and merely needs cleaning up, which is rarer than it sounds.
Both leave `-`, `.`, `_` and `~` alone. `encodeURIComponent` also leaves `!`, `'`, `(`, `)` and `*` unencoded, which is technically valid but occasionally trips up strict server-side parsers, particularly in older OAuth implementations.
The plus sign is a space, except when it is not
In `application/x-www-form-urlencoded` data, which is what HTML forms send and what most query strings are parsed as, `+` means a space. In a URL path, `+` is a literal plus. This inconsistency is why an email address like `user+tag@example.com` arrives as `user tag@example.com` when passed through a query string unencoded. Always encode a literal plus as `%2B`.
Double Encoding and How to Spot It
Encode a string twice and the percent signs from the first pass get encoded in the second. A space becomes `%20`, then the `%` becomes `%25`, giving `%2520`.
Seeing `%2520` in a log or an address bar tells you exactly what happened: something encoded a value that had already been encoded. The usual culprits are a framework that encodes automatically combined with code that also encodes manually, or a redirect URL passed through two layers of routing.
The fix is never to decode twice as a workaround. It is to find which layer is encoding and stop the other one. Decoding twice appears to work until a user submits a value containing a literal `%25`, at which point the extra decode corrupts real data.
The reverse problem exists too. A value that was never encoded but contains a `%` followed by two hex digits will be silently decoded into something else. A password of `100%25off` decodes to `100%off`, and the authentication fails for reasons the logs will not explain.
Encode or decode a URL
Both directions, with full UTF-8 support. Nothing leaves your browser.
Rules That Prevent Most URL Bugs
Four habits eliminate the large majority of encoding problems in practice.
Encode values, never whole URLs
Build the URL from encoded pieces rather than encoding the finished string. Use URLSearchParams in JavaScript and it handles the escaping and joining for you.
Encode once, at the boundary
Decide which layer owns encoding and make every other layer pass values through untouched. Most double-encoding comes from two layers each being helpful.
Store decoded, transmit encoded
What goes in the database should be the real value. Encoding is a transport concern, so apply it when constructing a URL and reverse it on arrival.
Never encode a URL you are about to store as a link
Encoding an href that is already valid turns its slashes and colons into escapes, producing a link that resolves relative to the current page. This is the cause of the mysterious 404 on a URL that looks correct.
Frequently Asked Questions
Related Tools
Keep Reading
Base64 Is Not Encryption: What It Does and Why It Exists
Three bytes become four characters, which is where the 33 percent size increase and the trailing equals signs come from. And why it hides nothing.
JSON Errors Explained: Trailing Commas, NaN and Other Rejections
Why valid-looking JSON fails to parse, how to read a parser's error position, and the number precision bug that silently corrupts large IDs.
camelCase, snake_case, kebab-case: Which Case Goes Where
The convention each language expects, the one language where capitalisation changes behaviour, and why acronyms break every rule.
Title and Meta Description Length: Pixels, Not Characters
Google truncates by rendered width, so a title of capitals runs out thirty characters early. Plus why it rewrites most descriptions anyway.