URL encoding has a single standard: RFC 3986. It defines exactly which characters are allowed in a URI and how to encode the rest. The spec is clear, unambiguous, and rarely the source of bugs.
The problem is that every implementation interprets it differently. JavaScript gives you two functions that encode different sets of characters. Browsers normalize URLs before sending them, changing what you encoded. The + character means space in one context and literal plus in another, because it comes from a different specification entirely. The spec is fine. The ecosystem is where things break.
And when they break, the error messages are useless. "404 Not Found" or "Page Not Available." Nothing says "the frontend encoded the user input wrong."
encodeURI vs encodeURIComponent
URLs only allow a limited set of characters: letters, digits, and a handful of special characters like -, _, ., and ~. Everything else must be percent-encoded. The browser handles this when you type in the address bar. But when you build URLs programmatically from user input, the responsibility shifts to your code.
JavaScript hands you two functions. They are not interchangeable:
const term = "C++ & JavaScript";
encodeURI(term);
// "C++%20&%20JavaScript" - & and + pass through
encodeURIComponent(term);
// "C%2B%2B%20%26%20JavaScript" - everything encoded
The reason both exist is that RFC 3986 distinguishes between characters that have special meaning in a URI (:, /, ?, &, #) and characters that encode actual data. encodeURI() preserves the special characters because it assumes you're encoding a full URI structure. encodeURIComponent() encodes everything, because it assumes you're encoding a single data value that happens to be inside a URI.
The problem is this API design is confusing for exactly the audience that needs it. Someone who knows enough to build a dynamic URL also knows enough to think "I need to encode this." encodeURI sounds like the right function. It's not. Use encodeURIComponent() for query parameter values. Use encodeURI() only for the path portion, and only when you're sure the path doesn't contain user input.
I've seen teams waste hours debugging a 400 error that traces back to a single wrong function call. The symptom is a query parameter missing from the server logs entirely. The cause is & in the user's input being interpreted as a parameter separator because encodeURI() left it unencoded.
The same split exists across languages. Python's urllib.parse.quote() encodes everything by default; you pass safe='' to match encodeURIComponent. PHP has both rawurlencode() (RFC 3986) and urlencode() (form-encoding). The naming tells you which spec each one follows.
Why %20 and + Are Not the Same
Percent-encoding comes from RFC 3986. The + convention comes from the application/x-www-form-urlencoded format in the HTML spec. In query strings, + decodes as space: that's the form-encoding rule. In the rest of the URL (path, host, fragment), + is a literal plus sign.
This spec conflict causes real bugs. A search for "C++" generates ?q=C++. Most web frameworks decode + as space in query strings: Express via the qs library, Go's url.ParseQuery, PHP's $_GET. So the server receives "C " (two spaces where the plus signs were). The value you expected is silently corrupted. The problem isn't that the framework picks the wrong spec; it's that the same character encodes different things depending on which part of the URL it's in.
If your user searches for "C++" and your frontend sends the value as ?q=C++ (because encodeURI() left + unencoded, or because user input directly contained a +), the server decodes it as "C ". The input was silently corrupted between your code and the server, and there's no error message telling you it happened.
The safe approach: use %20 exclusively and never rely on + encoding. %20 works identically in every URL part regardless of which spec the parser follows. The URL encoder on this site uses %20 by default for that reason: it's the encoding that never has to guess.
Browsers Normalize Behind Your Back
Browsers normalize URLs before sending them. They decode percent-encoded characters in the origin (protocol, host, port) and re-encode them in canonical form. The path gets similar treatment. This processing layer operates independently of your encoding, so the URL your code constructed may not match what the server receives.
A filename encoded as %E2%82%AC (the euro sign in UTF-8) can arrive decoded, re-encoded differently, or stripped entirely depending on the browser. The result is a 404 because the server sees a path that doesn't match any route.
This is visible in DevTools. Open the Network tab and compare the Request URL with what your code passed to fetch() or XMLHttpRequest. If they differ, browser normalization is the cause. The fix is to move user-supplied values out of paths and into query parameters, where encoding rules are more consistent and normalization is less aggressive.
The normalization also affects caching. A CDN that caches by URL path collapses two requests that differ only in encoding into one cache entry. That seems helpful until your application makes internal requests bypassing the browser's URL parser. Suddenly the same resource produces different normalized paths that compete for separate cache slots. The URLs look identical in the logs, but the cache hit rate drops for no obvious reason.
URL encoding works invisibly until it doesn't. The spec is clear. Read it once and the rules are straightforward. The implementations are the moving parts. When you see a 404 that should have worked, check the Network tab first, check what your encoding function actually outputs, and remember that + and %20 are not the same thing.