Skip to content
motifuse
Guide
14 min read

URL Encoding Explained: A Practical Guide

Learn how percent-encoding works, when to use encodeURIComponent, why spaces become %20 or +, and how to prevent broken links and double encoding.

The motifuse team

A URL can break even when every word in it looks correct. Put coffee & cream into a query value without encoding the ampersand, and the server may read cream as a second parameter. Encode an already encoded value, and %20 becomes %2520. Treat a literal plus sign like a space, and a search for C++ quietly becomes C followed by two spaces.

These failures all come from the same misunderstanding: a URL is not one undifferentiated string. It is a set of components, and characters such as /, ?, &, =, and # have structural jobs. URL encoding lets those same characters travel as data without being mistaken for separators.

This guide explains percent-encoding from first principles, shows the practical difference between encodeURI, encodeURIComponent, and URLSearchParams, and gives you a repeatable way to build and debug links without guessing.

Quick answer

URL encoding, usually called percent-encoding, represents a byte as a percent sign followed by two hexadecimal digits. A space can be written as %20; an ampersand used as data becomes %26. Non-ASCII text is first represented as UTF-8 bytes, so one visible character may produce several %HH sequences.

Encode values before inserting them into a URL component. Do not encode the entire finished URL with a component encoder, and do not encode a value that is already encoded. In browser JavaScript, prefer the URL and URLSearchParams APIs for complete links. Use encodeURIComponent when you specifically need to encode one component value.

The URL Encoder / Decoder uses encodeURIComponent and decodeURIComponent, making it useful for inspecting one value or reproducing an encoding problem without writing code.

Try it right here

URL Encoder / Decoder

Open full tool
Loading embedded tool...

Start with the anatomy of a URL

Consider this address:

text
https://example.com/products/coffee?q=dark roast&sort=price#reviews
ComponentExamplePurpose
SchemehttpsHow the resource is accessed
Hostexample.comThe server name
Path/products/coffeeThe resource hierarchy
Queryq=dark roast&sort=priceNamed parameters for the resource
FragmentreviewsA location or state within the resulting document

The punctuation between these components is meaningful. The first ? starts the query, & separates parameter pairs, = separates a name from its value, and # starts the fragment. If one of those characters belongs inside a value, it usually needs encoding so the parser does not assign it a structural role.

The URL Query Parser exposes the base URL, fragment, and each query parameter as separate fields. That makes it easier to see whether a suspicious ampersand is a delimiter or part of a value.

What percent-encoding actually does

RFC 3986, the generic URI syntax specification, defines a percent-encoded octet as % followed by two hexadecimal digits. The mechanism works at the byte level, not directly at the level of visible characters.

For ASCII characters, the relationship is easy to see:

CharacterASCII bytePercent-encoded form
Space20%20
&26%26
=3D%3D
#23%23
%25%25

Unicode characters are encoded as UTF-8 first. The word café becomes caf%C3%A9 because é is represented by the two UTF-8 bytes C3 A9. A check mark becomes %E2%9C%93, three sequences for three UTF-8 bytes. This is expected; the number of encoded triplets does not need to match the number of visible characters.

Hexadecimal letters are case-insensitive in encoded URLs, so %2f and %2F represent the same byte. RFC 3986 recommends uppercase hexadecimal for consistency, which is what common browser APIs produce.

Reserved and unreserved characters

RFC 3986 separates URL characters into two groups that matter here.

Unreserved characters are letters, digits, hyphen, period, underscore, and tilde. They can normally appear as data without encoding:

text
A-Z a-z 0-9 - . _ ~

Reserved characters may act as delimiters:

text
: / ? # [ ] @ ! $ & ' ( ) * + , ; =

Reserved does not mean forbidden. It means context matters. A slash should remain a slash when it separates path segments, but a slash that belongs inside a single path value may need to become %2F. An ampersand should remain unencoded between query parameters, but an ampersand inside a search phrase should become %26.

That is why “encode the URL” is incomplete advice. You need to know which component you are building and whether each reserved character is syntax or data.

Space as %20 versus +

Spaces have two common serialized forms:

  • General percent-encoding uses %20.
  • HTML form-style query serialization uses +.

JavaScript's encodeURIComponent("dark roast") returns dark%20roast. By contrast, URLSearchParams serializes the same query value as dark+roast. Both commonly represent a space in query data, but the plus convention is specific to form-style query parsing; a plus is not a universal substitute for a space everywhere in a URL.

This distinction matters for literal plus signs. If the intended value is C++, a query serializer should produce C%2B%2B. Constructing a query by concatenating raw text can cause a parser to interpret those plus signs as spaces.

encodeURI versus encodeURIComponent

The names are frustratingly similar, but the intended inputs differ.

FunctionIntended inputPreserves URL delimiters?Typical use
encodeURIA mostly complete URIYesEncoding spaces and Unicode in a complete URL while keeping its structure
encodeURIComponentOne component valueNo; encodes characters such as ?, &, =, and /Encoding a query value or one dynamic path segment
URLSearchParamsQuery parameter names and valuesBuilds the delimiters itselfCreating or editing a query string

MDN's encodeURIComponent reference notes that it encodes more characters than encodeURI, including characters that participate in URL syntax. Compare these two calls:

js
const value = "coffee & cream";

console.log(encodeURI(value));
// coffee%20&%20cream

console.log(encodeURIComponent(value));
// coffee%20%26%20cream

The first result leaves & untouched. That is unsafe if the result will become a query value because the ampersand may start another parameter. The component function encodes it as data.

Now consider a complete URL:

js
const address = "https://example.com/search?q=dark roast#top";

console.log(encodeURI(address));
// https://example.com/search?q=dark%20roast#top

console.log(encodeURIComponent(address));
// https%3A%2F%2Fexample.com%2Fsearch%3Fq%3Ddark%20roast%23top

The second output is valid encoded text, but it is no longer a navigable URL because every structural delimiter was encoded. It is suitable only if the entire address itself is being stored as a value inside another component.

The safer way to build query strings

String concatenation makes it easy to forget an encoder, add a second question mark, or place parameters after a fragment. The browser URL APIs keep structure and data separate.

js
const url = new URL("https://example.com/search");

url.searchParams.set("q", "coffee & cream");
url.searchParams.set("language", "C++");
url.searchParams.append("tag", "café");

console.log(url.href);
// https://example.com/search?q=coffee+%26+cream&language=C%2B%2B&tag=caf%C3%A9

The code never manually adds ?, &, or =. It supplies decoded values, and the serializer adds the right delimiters and encoding. MDN's URLSearchParams documentation also covers repeated keys: use append for multiple values and getAll when reading them.

Repeated parameters are legitimate:

js
const params = new URLSearchParams();
params.append("color", "blue");
params.append("color", "green");

console.log(params.toString());
// color=blue&color=green

console.log(params.getAll("color"));
// ["blue", "green"]

Do not replace append with set unless you intend to collapse repeated values into one. Servers and frameworks disagree on how they expose duplicate keys, so confirm the destination's behavior.

1. Separate structure from data

Write down the fixed base URL, path segments, query keys, query values, and fragment. If a user supplies a value, treat it as data rather than part of the URL syntax.

2. Choose the API for the component

Use URL for the overall address, URLSearchParams for query names and values, and encodeURIComponent for an isolated component when you cannot use the structured APIs. Avoid the obsolete JavaScript escape function; it does not implement modern URL encoding rules.

3. Encode once

Pass raw, decoded data to the encoder. If the input already contains sequences such as %20, determine whether they are real encoding or literal text before proceeding.

4. Parse and inspect the result

Use the URL Query Parser to verify the base, hash, and parameter values. A correct-looking address can still contain an unintended extra parameter or a fragment in the wrong place.

5. Test the destination

Open the link in the environment where it will be used. Redirects, proxies, application routers, and server frameworks can normalize URLs differently. Confirm both the destination page and the values received by the application.

The double-encoding trap

Double encoding happens when encoded text is passed through an encoder again:

js
const once = encodeURIComponent("hello world");
console.log(once);
// hello%20world

const twice = encodeURIComponent(once);
console.log(twice);
// hello%2520world

The second pass encodes the percent sign as %25. One decoding pass now returns hello%20world, not hello world. The OWASP explanation of double encoding describes how inconsistent decoding layers can also become a security problem when filters and application code interpret the same input differently.

Common causes include:

  • Encoding a value in the client and again on the server.
  • Passing an encoded string into URLSearchParams.append, which expects decoded data.
  • Encoding a complete URL before placing it into a library that encodes automatically.
  • Decoding at a proxy and decoding again in application code.

The fix is ownership: decide which layer encodes and which layer decodes. Make that contract explicit, then test with a value containing a space, percent sign, plus sign, ampersand, slash, and non-ASCII character.

Decoding and malformed input

decodeURIComponent reverses component encoding, but it is deliberately strict. A percent sign not followed by two hexadecimal digits, or a byte sequence that is not valid UTF-8, causes a URIError.

js
function safelyDecode(value) {
  try {
    return { ok: true, value: decodeURIComponent(value) };
  } catch {
    return { ok: false, value };
  }
}

console.log(safelyDecode("coffee%20shop"));
// { ok: true, value: "coffee shop" }

console.log(safelyDecode("save%now"));
// { ok: false, value: "save%now" }

Do not “repair” malformed input by repeatedly decoding until no percent signs remain. A percent sign may be legitimate data, and repeated decoding changes meaning. Reject the value, preserve it for inspection, or apply a narrowly defined recovery rule at the boundary where the data enters your system.

A fragment belongs at the end of a URL, after the query:

text
https://example.com/guide?source=newsletter#examples

If parameters are appended after #examples, they become part of the fragment rather than the query. This is a common failure in hand-built campaign links. The UTM Builder correctly encodes parameter values, while the guide to UTM parameters explains how to name those values consistently. When a destination includes a fragment, assemble the URL with the URL API or verify the finished structure before distributing it.

Encoding also matters before you put a long link into a QR code. A QR generator faithfully stores the text you provide; it does not know whether an unencoded ampersand broke the destination. Test the final URL first, then create it with the QR Code Generator. The companion article on how QR codes work covers scan testing and static versus redirect-based codes.

Security and privacy boundaries

Encoding is not encryption, validation, or sanitization. Anyone who can see the URL can decode its percent sequences. Do not put passwords, access tokens, private personal data, or other secrets into a query string merely because the characters look obscured.

URLs can appear in browser history, copied messages, analytics systems, server logs, monitoring tools, and referrer data. OWASP's application security guidance recommends excluding sensitive information from URLs because browsers and servers may retain them.

On the server, parse URL components before decoding values. RFC 3986 warns that decoding too early can turn encoded data into a delimiter and change how the URL is interpreted. Then validate the decoded value according to its actual purpose. Encoding prevents delimiter collisions; it does not make an untrusted path, redirect target, database filter, or HTML value safe.

Common mistakes and fixes

MistakeSymptomFix
Raw & inside a valueOne value becomes two parametersEncode the component or use URLSearchParams
Encoding the whole URL with encodeURIComponentThe link no longer navigatesEncode values, or use URL for the complete address
Encoding twice%20 becomes %2520Give one layer responsibility for encoding
Treating every + as a literal plusC++ becomes spacesSerialize the value so plus signs become %2B
Decoding before splitting componentsEncoded delimiters change structureParse first, decode component data second
Adding a query after #Server never receives the intended queryPut the query before the fragment
Using encoding to hide secretsSensitive data remains visible and retainedKeep secrets out of URLs
Ignoring repeated keysFilters or selections disappearUse append and verify server behavior

A compact debugging checklist

When a URL works in one place but not another, check it in this order:

  1. Parse the URL into scheme, host, path, query, and fragment.
  2. List every query key and value, including duplicates.
  3. Look for raw &, =, +, #, and stray percent signs inside values.
  4. Search for %25, which often signals a second encoding pass.
  5. Compare the decoded value with the value the application expected.
  6. Test a non-ASCII value to confirm UTF-8 is handled consistently.
  7. Follow redirects and confirm the query survives each hop.
  8. Remove sensitive data before sharing the URL in a ticket or chat.

This sequence keeps you from randomly replacing characters until the link appears to work. It also produces a small, reproducible case that another developer can inspect.

Frequently Asked Questions

What is URL encoding?
URL encoding is a way to represent bytes using a percent sign and two hexadecimal digits. It lets characters travel inside a URL component without being mistaken for delimiters or falling outside the permitted character set.
Should I use encodeURI or encodeURIComponent?
Use encodeURIComponent for one dynamic value such as a query value or path segment. encodeURI preserves structural characters and is intended for a mostly complete URI. For building full URLs in browser JavaScript, URL and URLSearchParams are usually safer than either function alone.
Why do spaces sometimes become %20 and sometimes +?
%20 is the general percent-encoded form of a space. Form-style query serialization, including URLSearchParams, uses + for spaces. A literal plus sign in query data must therefore be encoded as %2B.
What does %25 mean in a URL?
%25 represents the percent sign itself. Seeing %2520 often indicates double encoding: the percent sign in an existing %20 sequence was encoded on a second pass.
Can I decode an entire URL with decodeURIComponent?
You generally should not. Decoding the entire string can turn encoded delimiters into active URL syntax. Parse the URL into components first, then decode the component values at the appropriate layer.
Does URL encoding protect sensitive information?
No. Percent-encoding is reversible and provides no confidentiality. Sensitive values should not be placed in URLs because URLs may be stored in browser history, logs, analytics, and shared messages.
Why does decodeURIComponent throw an error?
It throws when a percent sign is not followed by two hexadecimal digits or when the encoded bytes are not valid UTF-8. Treat that as malformed input rather than repeatedly decoding or silently deleting characters.

The reliable rule

Build a URL from components, keep syntax separate from data, and encode each value exactly once. Use structured browser APIs when possible, inspect the parsed result, and test the finished link through its real redirects and destination.

Once that rule is in place, percent signs stop being mysterious. They become what they were designed to be: a precise way to carry data through a syntax that already uses punctuation for structure.

Put it into practice

Every guide comes with free tools to match.

Public tools open without sign-up. Local, upload, AI, and premium workspace steps are labeled clearly.

Explore all tools