URL Encoding and Decoding: Complete Guide with Examples
You have a URL with spaces, ampersands, or non-ASCII characters. If you drop them in raw, the URL breaks. URL encoding (also called percent-encoding) replaces unsafe characters with %XX hex codes so URLs work everywhere.
This guide covers the why, the how, and the subtle differences between encoding functions that trip people up.
Why URL Encoding Exists
URLs can only contain a specific set of characters defined by RFC 3986:
Unreserved: A-Z a-z 0-9 - _ . ~
Reserved: : / ? # [ ] @ ! $ & ' ( ) * + , ; =
Everything else -- spaces, accented characters, emoji, most punctuation -- must be percent-encoded. A space becomes %20. An ampersand becomes %26. The Japanese character becomes %E6%97%A5.
If you do not encode these characters, different systems will interpret your URL differently. A space might be treated as the end of the URL. An & might be interpreted as a query parameter separator when you meant it as literal text.
JavaScript: encodeURIComponent vs encodeURI
This is the most common source of confusion. JavaScript has two encoding functions, and using the wrong one produces broken URLs.
encodeURIComponent -- For Values
Use this when encoding a part of a URL (a query parameter value, a path segment):
const searchQuery = 'hello world & goodbye';
const url = `https://example.com/search?q=${encodeURIComponent(searchQuery)}`;
// https://example.com/search?q=hello%20world%20%26%20goodbye
encodeURIComponent encodes everything except: A-Z a-z 0-9 - _ . ~ ! ' ( ) *
It encodes / ? # & = + @ and all other special characters. This is what you want for parameter values, because those characters have meaning in URL structure.
encodeURI -- For Full URLs
Use this when you have a complete URL and just want to make it safe:
const url = 'https://example.com/path/to file?q=hello world';
encodeURI(url);
// https://example.com/path/to%20file?q=hello%20world
encodeURI does NOT encode: : / ? # [ ] @ ! $ & ' ( ) * + , ; =
It preserves the URL structure (protocol, slashes, query separator) and only encodes characters that are invalid everywhere.
When to Use Which
// CORRECT: Encoding a query value
const tag = 'C++ & C#';
const url = `https://api.example.com/search?tag=${encodeURIComponent(tag)}`;
// https://api.example.com/search?tag=C%2B%2B%20%26%20C%23
// WRONG: encodeURI on a value (& is not encoded, breaks the URL)
const url2 = `https://api.example.com/search?tag=${encodeURI(tag)}`;
// https://api.example.com/search?tag=C++%20&%20C# <-- broken!
// CORRECT: Encoding a full URL with spaces
const fullUrl = 'https://example.com/my path/file name.pdf';
encodeURI(fullUrl);
// https://example.com/my%20path/file%20name.pdf
// WRONG: encodeURIComponent on a full URL (slashes get encoded)
encodeURIComponent(fullUrl);
// https%3A%2F%2Fexample.com%2Fmy%20path%2Ffile%20name.pdf <-- broken!
Rule of thumb: Use encodeURIComponent for values, encodeURI for complete URLs with known structure.
URLSearchParams (The Modern Way)
For query parameters, skip manual encoding entirely:
const params = new URLSearchParams({
q: 'hello world',
lang: 'C++',
page: '1'
});
const url = `https://example.com/search?${params}`;
// https://example.com/search?q=hello+world&lang=C%2B%2B&page=1
URLSearchParams handles encoding automatically. Note that it encodes spaces as + (which is valid in query strings per the HTML spec) rather than %20.
Decoding
decodeURIComponent('hello%20world%20%26%20goodbye');
// "hello world & goodbye"
decodeURI('https://example.com/my%20path/file%20name.pdf');
// "https://example.com/my path/file name.pdf"
// URLSearchParams decodes automatically
const params = new URLSearchParams('q=hello+world&lang=C%2B%2B');
params.get('q'); // "hello world"
params.get('lang'); // "C++"
Python: urllib.parse
from urllib.parse import quote, quote_plus, unquote, unquote_plus, urlencode
# Encode a path segment (spaces become %20)
quote('hello world & goodbye')
# 'hello%20world%20%26%20goodbye'
# Encode a query value (spaces become +)
quote_plus('hello world & goodbye')
# 'hello+world+%26+goodbye'
# Build query string from dict
urlencode({'q': 'hello world', 'lang': 'C++', 'page': 1})
# 'q=hello+world&lang=C%2B%2B&page=1'
# Decode
unquote('hello%20world') # 'hello world'
unquote_plus('hello+world') # 'hello world'
Python Equivalents to JavaScript
| JavaScript | Python | Spaces |
encodeURIComponent() | quote(s, safe='') | %20 |
encodeURI() | quote(s, safe=':/?#[]@!$&\'()*+,;=') | %20 |
URLSearchParams | urlencode() | + |
Characters That Need Encoding
Here is a quick reference for the most common characters:
| Character | Encoded | Why It Needs Encoding |
| Space | %20 or + | Terminates URLs in many contexts |
& | %26 | Separates query parameters |
= | %3D | Separates key from value |
? | %3F | Starts query string |
# | %23 | Starts fragment/anchor |
% | %25 | Escape character itself |
+ | %2B | Interpreted as space in query strings |
/ | %2F | Path separator |
@ | %40 | Username separator in URLs |
" | %22 | Terminates attribute values in HTML |
< > | %3C %3E | HTML injection risk |
Spaces: %20 vs + (The Eternal Debate)
Both are valid encodings for a space, but in different contexts:
%20-- RFC 3986 standard. Valid everywhere in a URL (path, query, fragment).+-- HTML form encoding (application/x-www-form-urlencoded). Valid only in query strings.
In practice, most servers accept both in query strings. But in URL paths, only %20 is correct. example.com/my+file.txt is a file literally named my+file.txt, not my file.txt.
Common Pitfalls
Double Encoding
// User input
const value = 'hello world';
// Encode once (correct)
const encoded = encodeURIComponent(value);
// 'hello%20world'
// Encode again (WRONG -- now %20 becomes %2520)
const doubleEncoded = encodeURIComponent(encoded);
// 'hello%2520world'
Double encoding happens when you pass already-encoded values through another encoding step. The % in %20 gets encoded to %25, producing %2520. The fix: encode once, at the point where you build the URL.
Encoding the Entire URL Including Protocol
// WRONG
encodeURIComponent('https://example.com/path?q=test');
// 'https%3A%2F%2Fexample.com%2Fpath%3Fq%3Dtest'
// RIGHT: Use encodeURI or build the URL from parts
const base = 'https://example.com/path';
const params = new URLSearchParams({ q: 'test' });
const url = `${base}?${params}`;
Non-ASCII Characters
encodeURIComponent('cafe'); // 'cafe' (ASCII, no encoding needed)
encodeURIComponent('caf\u00e9'); // 'caf%C3%A9' (UTF-8 encoded)
encodeURIComponent('Tokyo'); // 'Tokyo'
URL encoding works on the UTF-8 byte representation. Multi-byte characters produce multiple %XX sequences.
Debugging URL Encoding Issues
When a URL is not working and you suspect encoding issues:
- Copy the full URL and decode it to see the raw values
- Check for double encoding (
%25appearing in the URL is a red flag) - Verify spaces are encoded correctly for the context (path vs query)
- Check that
&and=in parameter values are encoded
For quick encoding and decoding during development, devdash.io (or textshifter.com) has a URL encoder/decoder tool that shows the transformation in real time. Useful for debugging API calls where you are not sure if the encoding is correct.
Summary
// Encode a value for a query parameter
encodeURIComponent(value)
// Build query strings (handles encoding automatically)
new URLSearchParams({ key: value }).toString()
// Encode a full URL (preserves structure)
encodeURI(url)
// Decode
decodeURIComponent(encodedValue)
The golden rule: encode values with encodeURIComponent, build query strings with URLSearchParams, and never encode the same string twice.