You have seen it a hundred times: a space in a link becomes %20, an ampersand becomes %26, a search for café turns into caf%C3%A9. That is URL encoding — also called percent-encoding — and it is what lets URLs carry characters that would otherwise break them.
This guide explains how percent-encoding works, which characters are safe and which must be escaped, the confusing case of + versus %20, and the difference between JavaScript's encodeURIComponent and encodeURI — the mix-up behind a huge share of malformed-URL bugs. It follows RFC 3986, the standard that defines URI syntax.
Why URLs Need Encoding
A URL is a compact grammar, not free-form text. Certain characters have structural jobs: ? starts the query string, # marks a fragment, & separates query parameters, / divides path segments, = pairs a key with a value. If your data contains one of those characters, putting it in raw would confuse the parser — a & inside a value looks exactly like the start of a new parameter.
URLs are also restricted to a small, safe set of ASCII characters. Spaces, most punctuation, and anything non-ASCII (accented letters, emoji, other scripts) are not allowed to appear literally. Percent-encoding is the escape mechanism that lets any byte ride inside a URL without ambiguity.
How Percent-Encoding Works
The rule is mechanical: take the byte you need to escape, and replace it with a % followed by that byte's value as two hexadecimal digits. A space is byte 0x20, so it becomes %20. An ampersand is 0x26, so it becomes %26. A # is 0x23 → %23.
Space 0x20 → %20
# 0x23 → %23
& 0x26 → %26
? 0x3F → %3F
/ 0x2F → %2F
= 0x3D → %3DFor non-ASCII characters, the character is first encoded to its UTF-8 bytes, and each byte is then percent-encoded individually. The letter é is two UTF-8 bytes, 0xC3 0xA9, so it becomes %C3%A9. That is why café turns into caf%C3%A9 — one character, two percent-escapes.
RFC 3986 also defines unreserved characters that never need encoding: the ASCII letters A–Z and a–z, digits 0–9, and the four symbols - _ . ~. Everything else is either reserved (structural) or unsafe, and should be percent-encoded when it appears as data rather than structure.
The '+' vs '%20' Confusion
Sometimes a space is %20, and sometimes it is +. Both can be correct — the difference is *where in the URL you are* and *which encoding rules apply*.
- In the path and general URI syntax (RFC 3986), a space is always
%20. A literal+here means an actual plus sign. - In a query string encoded as `application/x-www-form-urlencoded` — the format HTML forms submit — a space is encoded as
+, and a literal plus becomes%2B.
Decoders mirror this: form-style decoders turn + back into a space, while a strict URI decoder leaves + as a plus. If you are ever unsure how a + will be read, prefer %20 for spaces and %2B for literal pluses — they mean the same thing to every decoder.
encodeURIComponent vs encodeURI
JavaScript ships two encoding functions, and picking the wrong one is one of the most common URL bugs. The difference is what each one considers 'structural' and therefore leaves alone.
| Function | Purpose | Leaves reserved chars (/ ? & = #) alone? |
|---|---|---|
| encodeURI | Encode a whole URL | Yes — keeps them so the URL still works |
| encodeURIComponent | Encode a single value/component | No — escapes them too |
encodeURI assumes you are handing it a complete URL and must not break its structure, so it does *not* escape / ? & = # :. encodeURIComponent assumes you are handing it a single piece of data — a query value, a path segment — that must not be mistaken for structure, so it escapes those characters too. For anything you drop into a query parameter, you almost always want encodeURIComponent.
const q = "tom & jerry?";
encodeURI(q);
// "tom%20&%20jerry?" — & and ? survive, WRONG inside a value
encodeURIComponent(q);
// "tom%20%26%20jerry%3F" — fully escaped, safe as a value
// Building a URL: encode each value, not the whole thing
const url = `/search?q=${encodeURIComponent(q)}&page=1`;
// "/search?q=tom%20%26%20jerry%3F&page=1"// The modern, hard-to-get-wrong approach:
const url = new URL("https://api.example.com/search");
url.searchParams.set("q", "tom & jerry?");
url.searchParams.set("page", "1");
url.toString();
// "https://api.example.com/search?q=tom+%26+jerry%3F&page=1"URL Encoding and JSON in URLs
Passing JSON through a URL is a common need — a filter object, a state blob, a config for a shareable link. JSON is full of characters that are reserved or unsafe in URLs: {, }, ", :, ,, and spaces. Dropped in raw, they corrupt the query string. The fix is to percent-encode the entire JSON string as a single component before appending it.
const filter = { tag: "news", limit: 10 };
const json = JSON.stringify(filter);
// '{"tag":"news","limit":10}'
const param = encodeURIComponent(json);
// '%7B%22tag%22%3A%22news%22%2C%22limit%22%3A10%7D'
const url = `/list?filter=${param}`;
// Decode on the other side:
const back = JSON.parse(decodeURIComponent(param));For readable, verifiable results, decoding a URL locally beats guessing. A client-side encoder/decoder runs entirely in your browser, so you can paste an escaped URL, read the plain text, and confirm exactly what a parameter contains without sending it anywhere.
%2FURL Encode / DecodeOpen the tool→