You paste a URL into your browser bar and suddenly it's full of %20, %3A, and %2F. Or you're building an API call in JavaScript and your parameter values are getting mangled. Or you're staring at a query string wondering why the plus signs keep turning into spaces.
URL encoding — officially called percent encoding — is one of those foundational web concepts that trips up developers at every level. This guide breaks it down completely: the why, the how, the edge cases, and exactly which JavaScript function to use when.
Why URLs Need Encoding at All
URLs can only contain a specific set of characters. The internet was designed to transmit text using ASCII, which covers only 128 characters — the basic Latin alphabet, digits, and a handful of punctuation marks. Everything else has to be translated into a safe format first.
Beyond the character set limitation, certain characters have structural meaning in URLs. The / separates path segments. The ? begins the query string. The # starts the fragment. The & separates query parameters. The = separates keys from values.
If you want to pass a value that contains one of those characters, the URL parser needs to know you mean it as data, not structure. That's what percent encoding is for.
URL encoding is formally defined in RFC 3986 (Uniform Resource Identifier: Generic Syntax), published in 2005. It superseded the older RFC 2396. Any URL handling code that follows the standard should implement RFC 3986.
How Percent Encoding Works
The rule is simple: any character that isn't allowed in a URL gets replaced with a % sign followed by its two-digit hexadecimal UTF-8 byte value.
A space has the ASCII code 32. In hexadecimal, 32 is 20. So a space becomes %20.
An @ sign has ASCII code 64. In hexadecimal, 64 is 40. So @ becomes %40.
Notice the last example: the accented é in "café" encodes to %C3%A9 — two bytes, not one. That's because UTF-8 encodes characters outside the basic ASCII range as multiple bytes, and each byte gets its own %XX sequence.
Safe (Unreserved) Characters
These characters are always safe in a URL and never need to be encoded:
Common Encoded Characters Quick Reference
| Character | Encoded | Notes |
|---|---|---|
| Space | %20 | or + in form data |
| ! | %21 | exclamation mark |
| " | %22 | double quote |
| # | %23 | starts a fragment in URLs |
| $ | %24 | dollar sign |
| & | %26 | separates query params |
| ' | %27 | apostrophe / single quote |
| / | %2F | path separator |
| : | %3A | scheme separator |
| = | %3D | key=value separator |
| ? | %3F | begins query string |
| @ | %40 | at sign |
| + | %2B | or + means space in form data |
encodeURI vs encodeURIComponent
JavaScript provides two built-in functions for URL encoding. They look similar but do very different things, and picking the wrong one is the most common URL encoding mistake.
encodeURI — For complete URLs
encodeURI() encodes a full URL. It intentionally leaves reserved characters intact because they are part of the URL's structure:
// encodeURI — leaves URL structure characters alone encodeURI('https://example.com/search?q=hello world&lang=en') // → 'https://example.com/search?q=hello%20world&lang=en' // ✓ : // ? & = are preserved // ✓ space → %20 // Characters NOT encoded by encodeURI: // A-Z a-z 0-9 ; , / ? : @ & = + $ - _ . ! ~ * ' ( ) #
encodeURIComponent — For parameter values
encodeURIComponent() encodes a single component — a value that will be inserted into a URL. It encodes reserved characters like /, ?, #, &, and = because those would break the URL structure if they appeared raw inside a parameter value:
// encodeURIComponent — encodes everything except unreserved chars encodeURIComponent('hello world') // → 'hello%20world' encodeURIComponent('user@example.com') // → 'user%40example.com' encodeURIComponent('a/b?c=d&e=f') // → 'a%2Fb%3Fc%3Dd%26e%3Df' // ✓ / ? = & all encoded — safe inside a parameter value // Characters NOT encoded by encodeURIComponent: // A-Z a-z 0-9 - _ . ! ~ * ' ( ) // Correct usage: building a URL with dynamic values const query = 'who is the #1 developer?'; const url = `https://example.com/search?q=${encodeURIComponent(query)}`; // → 'https://example.com/search?q=who%20is%20the%20%231%20developer%3F'
Never use encodeURIComponent on a complete URL. It will encode the ://, ?, and & characters, turning a valid URL into a broken string. Use encodeURIComponent only on the individual values you're inserting into a URL.
Side-by-side comparison
| Character | encodeURI | encodeURIComponent |
|---|---|---|
| space | %20 | %20 |
| / | preserved | %2F |
| ? | preserved | %3F |
| # | preserved | %23 |
| & | preserved | %26 |
| = | preserved | %3D |
| @ | preserved | %40 |
| + | preserved | %2B |
The + Sign vs %20: When You See Both
You've probably noticed that sometimes a space in a URL shows up as %20 and other times as +. Both represent spaces — but they come from different encoding systems and should not be used interchangeably.
%20 — RFC 3986 percent encoding
This is the standard. When you use encodeURIComponent() in JavaScript, it always produces %20 for spaces. This is correct for all parts of a URL including paths and query strings.
+ for spaces — HTML form encoding
When an HTML <form> is submitted with method GET, the browser encodes the form data using application/x-www-form-urlencoded format. This format, inherited from HTML 2.0, encodes spaces as + instead of %20. A literal + in the data becomes %2B.
// JavaScript — form-style encoding (spaces as +) function formEncode(str) { return encodeURIComponent(str) .replace(/%20/g, '+') .replace(/[!'()*]/g, c => '%' + c.charCodeAt(0).toString(16).toUpperCase()); } // Decoding form-encoded strings function formDecode(str) { return decodeURIComponent(str.replace(/\+/g, '%20')); }
Most server-side frameworks handle both %20 and + as spaces when parsing query strings. But when writing client-side code, prefer %20 (via encodeURIComponent) for consistency.
Unicode and Multi-Byte Characters
URL encoding was designed for ASCII, but the web is global. Unicode characters — accented letters, Chinese characters, emoji — all need to be encoded before they can appear in a URL.
The process: Unicode character → UTF-8 bytes → each byte percent-encoded.
// é (U+00E9) → UTF-8: 0xC3 0xA9 → %C3%A9 encodeURIComponent('café') // → 'caf%C3%A9' // 中 (U+4E2D) → UTF-8: 0xE4 0xB8 0xAD → %E4%B8%AD encodeURIComponent('中文') // → '%E4%B8%AD%E6%96%87' // 😀 (U+1F600) → UTF-8: 0xF0 0x9F 0x98 0x80 → %F0%9F%98%80 encodeURIComponent('😀') // → '%F0%9F%98%80' // Modern browsers show the decoded form in the address bar // but transmit the encoded form in HTTP requests
Modern browsers automatically handle internationalized domain names (IDN) and Unicode in URLs. When you type a Chinese URL in Chrome, the browser encodes it correctly before sending the request. In the address bar, it shows the human-readable version.
Parsing Query Strings in JavaScript
The modern way to parse and build query strings in JavaScript is the URLSearchParams API. It handles encoding and decoding automatically:
// Parsing a query string const params = new URLSearchParams('q=hello+world&lang=en&page=1'); params.get('q') // → 'hello world' (+ decoded as space) params.get('lang') // → 'en' params.get('page') // → '1' // Building a query string const search = new URLSearchParams({ q: 'hello world', filter: 'price>100', tag: 'a&b' }); search.toString() // → 'q=hello+world&filter=price%3E100&tag=a%26b' // Note: URLSearchParams uses + for spaces (form encoding) // Parsing from a full URL const url = new URL('https://example.com/?q=foo&page=2'); url.searchParams.get('q') // → 'foo'
URLSearchParams.toString() uses + for spaces (form encoding), not %20. This is correct and compliant with the HTML specification for form submissions. For manually constructed URLs, use encodeURIComponent instead.
Debugging Encoded URLs
When you encounter a URL full of percent sequences, here's how to decode it quickly:
// In your browser console: decodeURIComponent('hello%20world%3F') // → 'hello world?' // In Node.js: decodeURIComponent('caf%C3%A9') // → 'café' // Handle malformed sequences without throwing: function safeDecode(str) { try { return decodeURIComponent(str); } catch { return str; } // return original on error } // Decode + as space too (form-encoded strings): function decodeFormValue(str) { return decodeURIComponent(str.replace(/\+/g, '%20')); }
You can also use browser DevTools: open the Network tab, click a request, and the browser shows you the decoded query parameters in a readable table.
Common URL Encoding Mistakes
Double-encoding
Encoding an already-encoded string is a common bug. %20 becomes %2520 (the % itself gets encoded to %25). Always check whether a string is already encoded before encoding it again.
const alreadyEncoded = 'hello%20world'; // Bug: encoding twice encodeURIComponent(alreadyEncoded) // → 'hello%2520world' ← %25 is an encoded % sign! // Fix: decode first, then encode encodeURIComponent(decodeURIComponent(alreadyEncoded)) // → 'hello%20world' ✓
Forgetting to encode parameter values
Directly interpolating user input into a URL without encoding is both a bug and a security risk. A value like a&b=c will break your query string and potentially inject unexpected parameters.
// ❌ Dangerous: unencoded user input in URL const userInput = 'a&b=c'; const badUrl = `/api?name=${userInput}`; // → '/api?name=a&b=c' ← injects a second parameter! // ✅ Safe: always encode parameter values const safeUrl = `/api?name=${encodeURIComponent(userInput)}`; // → '/api?name=a%26b%3Dc' ✓
Using encodeURI on a component
Using encodeURI on a parameter value will leave characters like & and = unencoded, which will break the URL. Always use encodeURIComponent for values that go inside a query string.
Server-Side Decoding
On the server side, query string values are automatically decoded by the framework before you access them. In Express.js, req.query.name gives you the decoded value. In PHP, $_GET['name'] is already decoded. In Python's Flask, request.args.get('name') is decoded.
You only need to manually decode if you're reading raw query strings from an HTTP library or writing a custom parser.
Encode when building URLs. Decode only when you need to display or process the value. If you're using a URL object or a framework's request handler, encoding and decoding are handled for you.
Quick Reference Summary
| Use case | Function | Notes |
|---|---|---|
| Encode a full URL | encodeURI() | Preserves /, ?, #, &, =, : |
| Encode a query param value | encodeURIComponent() | Encodes everything except A-Z a-z 0-9 - _ . ! ~ * ' ( ) |
| Decode any encoded URL | decodeURIComponent() | Throws on malformed sequences — use try/catch |
| Parse a query string | URLSearchParams | Handles + and %20 as spaces automatically |
| Form data (HTML forms) | URLSearchParams or manual | Uses + for spaces, %2B for literal + |