URL encoding (percent-encoding) is how arbitrary text is made safe to put in a URL. It looks trivial — %20 for a space — but choosing the wrong function, encoding twice, or confusing + with a space causes some of the most persistent bugs in web development. This guide explains the rules and shows the exact output of the common functions.
Examples were run with Node.js 24.21, Python 3.9.6 and curl 8.7.1. Try any of them in the URL encoder.
How percent-encoding works
A URL may only contain a limited set of ASCII characters, and some of those — : / ? # [ ] @ ! $ & ' ( ) * + , ; = — are reserved because they separate the parts of the URL. To include any other character, or a reserved character as data, the text is converted to UTF-8 bytes and each byte is written as % followed by two hexadecimal digits (RFC 3986).
- A space (byte
0x20) becomes%20. éis two UTF-8 bytes, so it becomes%C3%A9.€is three bytes:%E2%82%AC. The emoji👋is four:%F0%9F%91%8B.
Letters, digits and - . _ ~ never need encoding. Hex digits are case-insensitive: %2F and %2f mean the same byte (curl, for example, writes lowercase).
encodeURIComponent vs encodeURI
JavaScript has two built-in functions, and they are for different jobs. With the same input:
const s = "price=10€ & tax/fee?";
encodeURIComponent(s); // "price%3D10%E2%82%AC%20%26%20tax%2Ffee%3F"
encodeURI(s); // "price=10%E2%82%AC%20&%20tax/fee?"encodeURIComponentencodes everything exceptA–Z a–z 0–9 - _ . ! ~ * ' ( ). Use it for a single value: one query parameter, one path segment.encodeURIleaves reserved characters such as= & / ?alone, because it assumes you are passing a whole URL whose structure is already correct. Use it only to escape spaces and non-ASCII characters in a complete URL.
Using encodeURI on a value is a classic bug: in the output above, & and = survive unencoded, so a server would read tax/fee? as a separate parameter.
Plus signs, spaces and form encoding
HTML forms and URLSearchParams use a slightly different format, application/x-www-form-urlencoded, where a space is written as +:
new URLSearchParams({ q: "price=10€ & tax/fee?" }).toString();
// "q=price%3D10%E2%82%AC+%26+tax%2Ffee%3F"That means the meaning of + depends on which decoder reads it:
decodeURIComponent("a+b"); // "a+b" (plus stays a plus)
new URLSearchParams("q=a+b").get("q"); // "a b" (plus is a space)
new URLSearchParams("q=a%2Bb").get("q"); // "a+b"Most servers decode query strings the form way, so a literal plus sign must be sent as %2B. If phone numbers like +855… or base64 values arrive with spaces in them, something put a raw + in the query string.
Building URLs safely
Never concatenate raw values into a URL. This looks fine but silently loses data:
const q = "tom & jerry";
new URL(`https://api.example.com/search?q=${q}`).searchParams.get("q");
// "tom " (the & started a new parameter)Let the URL API do the encoding instead. It encodes each value correctly, including the plus signs in c++:
const url = new URL("https://api.example.com/search");
url.searchParams.set("q", "red shoes & socks");
url.searchParams.set("tag", "c++");
url.href;
// "https://api.example.com/search?q=red+shoes+%26+socks&tag=c%2B%2B"For path segments, encode each segment separately with encodeURIComponent, so a slash inside a value cannot create an extra path level:
"/files/" + encodeURIComponent("reports/2026 Q1.pdf");
// "/files/reports%2F2026%20Q1.pdf"Double encoding
Encoding a value that is already encoded turns every % into %25:
encodeURIComponent(encodeURIComponent("café"));
// "caf%25C3%25A9"The server decodes once and receives the literal text caf%C3%A9. If you see %25 in a URL, or search results stop matching for non-ASCII input, look for two layers that both encode — for example, your code calls encodeURIComponent and then an HTTP library encodes the parameters again. Encode in exactly one place, as close to the HTTP call as possible.
Decoding errors
JavaScript's decoders throw when a % is not followed by two hex digits, or when the bytes are not valid UTF-8:
decodeURIComponent("100%"); // URIError: URI malformed
decodeURIComponent("%C3"); // URIError: URI malformed (incomplete UTF-8 sequence)Wrap decoding of untrusted input in try/catch. The URL encoder reports where the first malformed sequence is. Python is more forgiving, which can hide the problem:
from urllib.parse import unquote
unquote("100%") # '100%'
unquote("%C3") # '\ufffd' (replacement character, no error)The same operations in Python and curl
from urllib.parse import quote, quote_plus, urlencode
s = "price=10€ & tax/fee?"
quote(s) # 'price%3D10%E2%82%AC%20%26%20tax/fee%3F' (keeps "/" by default!)
quote(s, safe="") # 'price%3D10%E2%82%AC%20%26%20tax%2Ffee%3F' (like encodeURIComponent)
quote_plus(s) # 'price%3D10%E2%82%AC+%26+tax%2Ffee%3F' (form style)
urlencode({"q": s, "tag": "c++"})
# 'q=price%3D10%E2%82%AC+%26+tax%2Ffee%3F&tag=c%2B%2B'Note that Python's quote leaves / unencoded unless you pass safe="" — the same trap as encodeURI.
# curl encodes a query value for you with -G and --data-urlencode
curl -G https://api.example.com/search --data-urlencode "q=price=10€ & tax/fee?"
# requests: https://api.example.com/search?q=price%3d10%e2%82%ac+%26+tax%2ffee%3fQuick reference
| You are encoding | JavaScript | Python |
|---|---|---|
| One query value or path segment | encodeURIComponent(v) | quote(v, safe="") |
| A whole query string from key/value pairs | new URLSearchParams(obj).toString() | urlencode(obj) |
| A complete URL with spaces or non-ASCII | new URL(str).href or encodeURI(str) | quote(str, safe=":/?#[]@!$&'()*+,;=") |
| Decoding a query value | new URLSearchParams(qs).get(k) | parse_qs(qs) or unquote_plus(v) |
When a request behaves strangely, paste the full URL into the URL parser on the URL encoder page to see every query parameter exactly as the server will decode it. For the wider debugging workflow, see How to debug API responses.