Encode individual values, not whole URLs. A URL is built from structural characters — ://, the / in a path, the ? and & that separate query parameters — and from the values you drop into it. Those structural characters must stay literal, or the address stops parsing; the values must be percent-encoded, or a stray & or = silently changes the URL's meaning. In JavaScript that maps to two functions: encodeURIComponent for each value, encodeURI only for a complete URL you assembled yourself. Encoding the whole thing with encodeURIComponent is the classic mistake — it mangles :// into %3A%2F%2F and breaks the link.
That's the summary an AI overview gives you. Here's what it can't: a component-by-component map of which characters each function touches, why escape() is a trap, and the security caveats that come with getting this wrong.
A URL is components, and each has different rules
A URL has a fixed shape — scheme://host:port/path?query#fragment — and the encoding rule changes as you move across it. The diagram below breaks a real URL into its parts and shows which characters are structure (leave them alone) and which are values (encode them).
The protocol (http, https) is never encoded. The host follows internationalized-domain rules (punycode, not percent-encoding). The path encodes special characters inside each segment but preserves the / separators. The query string needs the most care — spaces, ampersands, and equals signs inside a value all need encoding, while the ?, &, and = that structure the query must stay literal. The fragment behaves much like the path.
The four functions: what each one touches
JavaScript ships two encoders and two decoders — plus one deprecated pair you should never reach for. This is the whole map:
| Function | Encodes | Leaves alone | Use it for |
|---|---|---|---|
encodeURIComponent(str) | Everything except unreserved chars — including the reserved ; / ? : @ & = + $ , # and space | A–Z a–z 0–9 - _ . ! ~ * ' ( ) | A single value: one query-string value, one path segment |
encodeURI(str) | Unsafe chars like space, but preserves URL structure | Unreserved chars plus reserved ; / ? : @ & = + $ , # | A complete URL you built and want to tidy up |
escape(str) ⚠️ deprecated | Most chars, but as broken %uXXXX for non-ASCII | @ * _ + - . / and ASCII letters/digits | Nothing — do not use |
decodeURIComponent(str) | — (reverses encodeURIComponent) | — | Decoding a single value back to plain text |
decodeURI(str) | — (reverses encodeURI) | — | Decoding a whole URL back |
The key row is the difference between the two encoders: encodeURI deliberately leaves the reserved set ; / ? : @ & = + $ , # untouched so a full URL still parses, whereas encodeURIComponent encodes those same characters so a value can never masquerade as structure. Both leave the eleven unreserved characters (A–Z a–z 0–9 - _ . ! ~ * ' ( )) alone, per RFC 3986.
Why avoid escape()? It predates UTF-8 percent-encoding. For non-ASCII input it emits the non-standard %uXXXX sequence that servers and RFC 3986 do not understand, and it fails to encode characters like + and @ that genuinely need it inside a query value. Its partner unescape() shares the same flaws. Treat both as removed.
Building a URL correctly
The right pattern encodes each value on its own and joins the pieces with literal structural characters you write yourself:
const base = 'https://api.example.com/search';
const term = 'books & media';
const url = `${base}?q=${encodeURIComponent(term)}&sort=${encodeURIComponent('price asc')}`;
// → https://api.example.com/search?q=books%20%26%20media&sort=price%20asc
Notice the & between parameters stays literal (it separates them), but the & inside the value books & media becomes %26 — otherwise the server would read media as a second parameter. Run encodeURIComponent on the whole finished URL and you would instead get https%3A%2F%2Fapi.example.com..., a string no browser can navigate to.
Other languages mirror this split. Python's urllib.parse offers quote() (safe-preserves / by default, path-oriented) and quote_plus() (query-oriented, encodes / and turns spaces into +), with urlencode() to build a whole query string from a dict. Java has URLEncoder.encode(...) for form data. Whatever the language, the principle holds: reach for the component encoder for values, not the whole-URL one.
Best practices and common pitfalls
Avoid encoding URLs by hand with string replacement — the built-in functions handle edge cases (multi-byte UTF-8, surrogate pairs, the exact reserved set) that ad-hoc code gets wrong. Test with the nasty inputs: empty strings, strings that are nothing but special characters, very long strings, and international text such as café or 日本語. Verify a round-trip: encode a value, decode the result, and confirm you recover the original exactly — that catches character-set bugs early.
Document what your API expects. "The q parameter must be URL-encoded" or "send parameters as application/x-www-form-urlencoded" removes an entire class of integration bugs. Ambiguity here is one of the most common sources of support tickets between teams.
Security implications
URL encoding keeps user input from breaking out of its parameter context. Without it, a value like ?admin=true supplied as a username could inject an extra parameter and change the URL's structure; encoded to %3Fadmin%3Dtrue, it stays a harmless literal string. That is a correctness win — but encoding is not a security control.
Double-encoding attacks exploit systems that decode more than once: a filter blocking ../ (directory traversal) may miss %2e%2e%2f, which a later decode step turns back into ../. Always validate and sanitize after decoding, decode input exactly once, and never treat "it was encoded" as "it is safe." (Our double-encoding prevention guide covers the defensive patterns in depth.)
Cross-site scripting needs a different layer entirely. A URL parameter echoed into an HTML page requires HTML escaping, not just URL encoding — the two operate in different contexts. Layer the right encoding for each place a value lands: URL-encode it in the URL, HTML-escape it in the markup, JavaScript-escape it in a script.
Tools and resources
The URL Encoder/Decoder tool above encodes and decodes with automatic format detection: paste any URL or text and it works out whether to encode or decode, highlights the characters that need encoding, and shows the UTF-8 byte breakdown for multi-byte characters. Everything runs client-side, so your URLs never leave your browser.
For the command line, use your language's URL library rather than sed-style hacks:
# Linux/macOS
echo 'hello world' | python3 -c 'import sys, urllib.parse; print(urllib.parse.quote(sys.stdin.read().strip()))'
# Windows PowerShell
[System.Uri]::EscapeDataString('hello world')
And in any browser console, encodeURIComponent('test string') and decodeURIComponent('%20') give you instant checks while debugging.
Conclusion
Knowing which URL parts to encode is the whole game: encode the values in your query parameters and path segments, keep the structural characters that define the URL literal, and let the component encoder — encodeURIComponent, quote(), EscapeDataString — draw that line for you. Skip escape(), decode exactly once, and remember that encoding is for syntactic correctness, not security. Get the split right and your URLs stay both valid and safe.
Ready to encode or decode URLs correctly? Use our URL Encoder/Decoder tool for instant, accurate URL encoding with automatic format detection, UTF-8 support, and client-side privacy.