URL Encoding Explained: Percent-Encoding and Special Characters
By Álvaro G.V. · Updated
URL encoding (percent-encoding) replaces every byte that isn't allowed in a URL with % and two hex digits: a space becomes %20, & becomes %26, and é becomes %C3%A9 (its two UTF-8 bytes). In JavaScript, encode each query value or path segment with encodeURIComponent, or let URLSearchParams do it for you. This guide covers how the encoding works, which function to use in each language, and the mistakes that cause broken links and double-encoded %2520 URLs.
To encode or decode a string as you read, use the URL encoder and decoder.
Why URLs Need Encoding
The URL specification (RFC 3986) restricts URLs to a safe subset of ASCII characters. Characters outside this set — spaces, Unicode letters, many punctuation marks — would be ambiguous or break URL parsing. For example, a space in a URL looks identical to the end of the URL in many contexts, and & separates query parameters, so it can't appear unencoded as a parameter value.
Percent-encoding (also called URL encoding) is the mechanism for safely including any character in a URL by representing it as % followed by its two-digit hexadecimal ASCII code.
The Percent-Encoding Mechanism
Each unsafe character is replaced with %XX, where XX is the uppercase hexadecimal representation of the character's byte value in UTF-8:
| Character | Encoded | Reason |
|---|---|---|
| Space | %20 | Not allowed in URLs |
| # | %23 | Fragment delimiter |
| % | %25 | Encoding prefix itself |
| & | %26 | Query param separator |
| + | %2B | Means space in form encoding |
| = | %3D | Key-value separator |
| ? | %3F | Query string start |
| / | %2F | Path separator |
| é (U+00E9) | %C3%A9 | Non-ASCII (UTF-8: 2 bytes) |
| € (U+20AC) | %E2%82%AC | Non-ASCII (UTF-8: 3 bytes) |
Reserved vs Unreserved Characters
RFC 3986 defines two important categories:
Unreserved characters — safe anywhere in a URL, never need encoding:
A-Z a-z 0-9 - _ . ~Reserved characters — have special meaning in URL syntax. They must be percent-encoded when used as data (not as structural delimiters):
: / ? # [ ] @ ! $ & ' ( ) * + , ; =Everything else — spaces, non-ASCII Unicode, control characters — must always be encoded. For binary data,URL-safe Base64 is usually more compact than percent-encoding every byte.
encodeURI vs encodeURIComponent
JavaScript provides two encoding functions, and choosing the wrong one is a common source of bugs:
encodeURI(url)
Encodes a complete URL. It leaves reserved characters unencoded because they are structural — ://, /, ?, &, =, and # all have specific meaning in a full URL.
encodeURI("https://example.com/search?q=hello world&lang=en")
// → "https://example.com/search?q=hello%20world&lang=en"
// Note: & and = are left intact (they're structural)encodeURIComponent(value)
Encodes a single value for use inside a URL component. It encodes reserved characters — including /, ?, #, &, and = — because those characters would break the URL structure if they appeared unencoded in a parameter value.
encodeURIComponent("price=10¤cy=USD/EUR")
// → "price%3D10%26currency%3DUSD%2FEUR"
// Note: = / & all encoded (they'd break query parsing unencoded)
// Building a URL safely:
const query = encodeURIComponent(userInput);
const url = `https://api.example.com/search?q=${query}`;Rule of thumb: use encodeURIComponent for individual values you're inserting into a URL. Use encodeURI only if you have a complete URL string and just need to make it safe without breaking its structure.
URLSearchParams and the URL API
For query strings, the safest option is not to encode by hand at all. URL and URLSearchParams encode each value as you set it:
const url = new URL("https://example.com/search");
url.searchParams.set("q", "rock & roll");
url.searchParams.set("next", "/cart?id=1");
url.href;
// → "https://example.com/search?q=rock+%26+roll&next=%2Fcart%3Fid%3D1"Note the + for spaces: URLSearchParams uses the HTML form format (application/x-www-form-urlencoded), not plain percent-encoding. Any server that parses query strings as form data reads it back correctly, but the three JavaScript encoders do produce different output for the same input:
| Input | encodeURIComponent | encodeURI | URLSearchParams |
|---|---|---|---|
| space | %20 | %20 | + |
| + | %2B | + | %2B |
| & = ? / # | %26 %3D %3F %2F %23 | & = ? / # | %26 %3D %3F %2F %23 |
| ~ | ~ | ~ | %7E |
| ! ' ( ) * | ! ' ( ) * | ! ' ( ) * | %21 %27 %28 %29 * |
| é | %C3%A9 | %C3%A9 | %C3%A9 |
encodeURIComponent leaves ! ' ( ) * alone even though RFC 3986 reserves them. That is harmless in query strings but matters when a spec requires strict RFC 3986 encoding, such as OAuth 1.0 signatures and AWS Signature Version 4. There you need an extra step:
const encodeRFC3986 = (s) =>
encodeURIComponent(s).replace(/[!'()*]/g, (c) => "%" + c.charCodeAt(0).toString(16).toUpperCase());
encodeRFC3986("it's (done)*"); // → "it%27s%20%28done%29%2A"Worked example: a URL inside a URL
A login page that sends the user back afterwards needs the return address as a query value. That address has its own ?, & and /, so it must be encoded as a whole:
const returnTo = "https://app.example.com/cart?id=1&step=2";
// Wrong: the server sees returnTo=https://app.example.com/cart?id=1 and a separate step=2
"https://login.example.com/?returnTo=" + returnTo;
// Right
"https://login.example.com/?returnTo=" + encodeURIComponent(returnTo);
// → "https://login.example.com/?returnTo=https%3A%2F%2Fapp.example.com%2Fcart%3Fid%3D1%26step%3D2"The server decodes the value once and gets the original URL back. Validate it against an allow-list of your own hosts before redirecting, or the parameter becomes an open redirect.
URL Encoding in Other Languages
Most languages ship two functions: one for paths (space as %20) and one for form-style query strings (space as +). Picking the wrong one is the usual cause of stray plus signs or %20 showing up in search terms.
| Language | Path segment / generic (space → %20) | Query string, form style (space → +) |
|---|---|---|
| JavaScript | encodeURIComponent(s) | new URLSearchParams({ q: s }) |
| Python | urllib.parse.quote(s, safe="") | urllib.parse.urlencode({"q": s}), quote_plus(s) |
| PHP | rawurlencode($s) | urlencode($s), http_build_query() |
| Go | url.PathEscape(s) | url.QueryEscape(s), url.Values{}.Encode() |
| Java | (no built-in for paths; use a URI builder) | URLEncoder.encode(s, StandardCharsets.UTF_8) |
| curl | (encode by hand) | curl -G --data-urlencode "q=rock & roll" URL |
Watch Python's default: quote(s) treats / as safe and leaves it unencoded, so quote("a/b") returns a/b. Pass safe="" when the value is a single segment or a query value.
Query Strings and Form Data
HTML form submissions encode data using the application/x-www-form-urlencoded format, which has a quirk: spaces are encoded as + rather than %20. This applies only to query string values in form submissions — in URL paths, + is a literal plus sign.
When processing form data on the server, use a form-aware decoder (like PHP's $_GET, Python's urllib.parse.parse_qs, or Express's req.query) rather than a raw URL decoder, so + is correctly converted to a space.
Common Mistakes
Double encoding
Encoding an already-encoded string encodes the % sign itself: hello%20world becomes hello%2520world. Decode once to recover the original. Avoid double encoding by not encoding values that are already encoded.
Using encodeURI for parameter values
encodeURI does not encode & and =, so a parameter value containing those characters will corrupt the query string. Always use encodeURIComponent for individual values.
Decoding a form-encoded query with decodeURIComponent
decodeURIComponent("a+b%2Bc") returns a+b+c: it does not treat + as a space, so a search for "a b+c" arrives as "a+b+c". Parse query strings with new URLSearchParams(location.search).get("q"), which returns a b+c.
A stray % crashes decoding
decodeURIComponent("100%") and decodeURIComponent("%E0%A4%A") both throw URIError: URI malformed (Firefox says "malformed URI sequence"). Any user-controlled value, such as a path segment from a hand-typed URL, can contain one, so wrap the call in try/catch and answer with a 400 instead of crashing.
Forgetting that % must be encoded
If your data contains literal percent signs (e.g. "50% off"), they must be encoded as %25 before inserting into a URL. Both encodeURI and encodeURIComponent handle this automatically.
Encode and Decode URLs Online
Need to quickly encode a URL or decode a percent-encoded string? The URL Encoder on DevEssentials supports both encodeURIComponent and encodeURI modes — all client-side, nothing sent to any server.
Ready to encode or decode a URL? Open the URL Encoder →
Frequently Asked Questions
What is the difference between %20 and + for spaces in URLs?▾
%20 is the standard percent-encoding for a space character per RFC 3986. It is safe everywhere in a URL — in path segments, query strings, and fragments.
+ is part of the HTML form encoding format (application/x-www-form-urlencoded). When a browser submits a GET form, spaces in field values are encoded as +, not %20. A + only means 'space' in a query string context when decoded by a form parser. In a URL path segment, + is a literal plus sign.
Rule of thumb: use %20 for spaces in paths and URIs; be aware that + in query strings may mean space depending on how the server decodes it.
Which characters must be percent-encoded in a URL?▾
RFC 3986 defines two categories. Unreserved characters are safe anywhere and never need encoding: A–Z, a–z, 0–9, hyphen (-), underscore (_), period (.), and tilde (~).
Reserved characters have special meaning in URL syntax: : / ? # [ ] @ ! $ & ' ( ) * + , ; =. They must be percent-encoded when used as data (not as structural delimiters). Everything else — non-ASCII Unicode, spaces, control characters — must also be percent-encoded.
What is the difference between encodeURI and encodeURIComponent?▾
encodeURI(url) encodes a complete URL. It leaves reserved characters (: / ? # @ etc.) unencoded because they are part of the URL structure. Use this when you have a full URL string that you want to make safe without breaking its structure.
encodeURIComponent(value) encodes a single value intended for use inside a URL component (typically a query parameter value or path segment). It encodes reserved characters like /, ?, #, &, and = because those characters would break the URL structure if they appeared unencoded inside a parameter value. Use this for individual values you're building into a URL.
What is double encoding and how do I avoid it?▾
Double encoding happens when an already-encoded string gets encoded again. The percent sign % itself gets encoded to %25, so %20 (space) becomes %2520 (literal '%20' instead of a space).
This often happens when encoding a URL that already contains encoded values, or when a framework automatically encodes values that you've already encoded manually. To avoid it: encode values exactly once before building the URL, and don't encode a URL that has already been constructed.
How does URL encoding handle Unicode characters?▾
Unicode characters are first converted to their UTF-8 byte sequence, then each byte is percent-encoded. For example, the é character (U+00E9) is represented as two bytes in UTF-8: 0xC3 0xA9, which encodes to %C3%A9 in a URL.
This is why a single Unicode character can produce multiple percent-encoded sequences. The decodeURIComponent() function in JavaScript automatically converts these UTF-8 sequences back to Unicode characters.
Related guides
What Is Base64 Encoding and How Does It Work?
Base64 converts binary data into ASCII text using 64 characters. Learn why it exists, how the encoding works step by step, and when to use it.
REST API Design Best Practices: A Practical Guide
The conventions that make REST APIs intuitive and consistent. Covers URL design, HTTP methods, status codes, versioning, pagination, and error responses.
Regex Cheat Sheet for JavaScript Developers
Anchors, quantifiers, character classes, flags, named groups, lookaheads, and copy-paste patterns for email, URL, date, and more — with JavaScript method examples.