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.

RFC 3986

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.

Original text
hello world user@example.com search?q=foo&lang=en price: $9.99 café au lait
After encodeURIComponent
hello%20world user%40example.com search%3Fq%3Dfoo%26lang%3Den price%3A%20%249.99 caf%C3%A9%20au%20lait

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:

A–Zsafe
a–zsafe
0–9safe
-safe
_safe
.safe
~safe

Common Encoded Characters Quick Reference

CharacterEncodedNotes
Space%20or + in form data
!%21exclamation mark
"%22double quote
#%23starts a fragment in URLs
$%24dollar sign
&%26separates query params
'%27apostrophe / single quote
/%2Fpath separator
:%3Ascheme separator
=%3Dkey=value separator
?%3Fbegins query string
@%40at sign
+%2Bor + 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:

JavaScript
// 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:

JavaScript
// 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'
Common Mistake

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

CharacterencodeURIencodeURIComponent
space%20%20
/preserved%2F
?preserved%3F
#preserved%23
&preserved%26
=preserved%3D
@preserved%40
+preserved%2B
Try the URL Encoder / Decoder
Encode or decode URLs and query strings instantly — with encodeURI, encodeURIComponent, and form-data modes.
Open URL Encoder

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.

HTML form encoding example




// 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'));
}
Tip

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.

Unicode encoding examples
// é (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:

URLSearchParams API
// 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'
Note on URLSearchParams and +

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:

Quick decoding methods
// 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.

Double-encoding bug
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.

Always encode user input
// ❌ 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.

Rule of Thumb

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.

Encode, Decode, and Parse URLs Instantly
Paste any URL and see its query parameters decoded in a table. Switch between encodeURI, encodeURIComponent, and form-data encoding modes.
Open URL Encoder / Decoder

Quick Reference Summary

Use caseFunctionNotes
Encode a full URLencodeURI()Preserves /, ?, #, &, =, :
Encode a query param valueencodeURIComponent()Encodes everything except A-Z a-z 0-9 - _ . ! ~ * ' ( )
Decode any encoded URLdecodeURIComponent()Throws on malformed sequences — use try/catch
Parse a query stringURLSearchParamsHandles + and %20 as spaces automatically
Form data (HTML forms)URLSearchParams or manualUses + for spaces, %2B for literal +