Developer

URL Encode and Decode: How Percent Encoding Works

HR
Hassaan Rasheed
· August 31, 2026 9 min read
URL Encode and Decode: How Percent Encoding Works

You build a search endpoint that takes a query parameter. Everything works until a user searches for C++ tutorials and the request breaks. The plus signs disappear. The URL you construct looks fine in your code, but what actually gets sent to the server is C tutorials with two extra + characters that are now interpreted as spaces somewhere in the chain.

The URL Encode / Decode tool at ToolCenterHub encodes and decodes strings in your browser. Paste a raw string to get the percent-encoded version, or paste a percent-encoded value to decode it back to plain text. This guide explains how percent encoding works, when to use each JavaScript encoding function, which characters require encoding and why, and the most common encoding bugs that break API integrations.

What Is URL Encoding and Why Does It Exist?

A URL can only contain a specific set of characters: letters, digits, and a small set of symbols that have defined roles in URL syntax. Hyphens, underscores, periods, and tildes are safe. Forward slashes separate path segments. Question marks start query strings. Ampersands separate query parameters. Hash symbols start fragment identifiers.

Every other character must be encoded before it can appear in a URL. Encoding converts the character to a percent sign followed by two uppercase hexadecimal digits representing the character's byte value in UTF-8. A space has a byte value of 32, which is 20 in hexadecimal, so it becomes %20. The copyright symbol is a two-byte UTF-8 sequence (0xC2 0xA9) so it becomes %C2%A9.

This matters because URLs are transmitted as plain text over HTTP. If a URL contains a space or a non-ASCII character, different browsers, proxies, and servers may interpret or encode it differently. Percent encoding defines one unambiguous representation for every character, which means a percent-encoded URL looks the same everywhere in the transmission chain.

The formal standard is RFC 3986, which defines the URL syntax and specifies exactly which characters must be encoded, which are allowed unencoded, and which are reserved as delimiters. Understanding this standard explains why there are two different encoding functions in JavaScript instead of one.

How Does URL Encoding Work?

Encoding a string to a percent-encoded URL component happens in two steps. First, the string is converted to bytes using UTF-8 encoding. Second, each byte that is not in the unreserved character set is replaced with % followed by two hexadecimal digits.

The unreserved characters in RFC 3986 are: A-Z, a-z, 0-9, hyphen, underscore, period, and tilde. These never need encoding. Everything else either must be encoded or is a reserved character that means something specific in URL structure. This is why UUIDs appear safely in URL path segments without any encoding: they contain only hex digits and hyphens, both of which are unreserved.

Reserved characters fall into two groups: general delimiters (:, /, ?, #, [, ], @) and sub-delimiters (!, $, &, ', (, ), *, +, ,, ;, =). When these characters appear as URL structure (a slash separating path segments, an ampersand separating parameters), they are left unencoded. When they appear as literal values within a component (an ampersand inside a parameter value), they must be encoded.

This is the distinction that drives the two JavaScript encoding functions.

What Is the Difference Between encodeURI and encodeURIComponent?

JavaScript has two encoding functions, and choosing the wrong one breaks URLs in ways that are not always obvious.

encodeURI(url) encodes a complete URL. It leaves all characters that have structural meaning in a URL unencoded: colons, slashes, question marks, hash symbols, brackets, ampersands, equals signs, and the other sub-delimiters. Use encodeURI when you have a complete URL string that might contain non-ASCII characters or spaces, but where the URL structure is already correct.

encodeURI("https://example.com/search?q=hello world&lang=en")
// Result: "https://example.com/search?q=hello%20world&lang=en"
// The & and = are left unencoded because they're URL structure

encodeURIComponent(value) encodes a single URL component. It encodes everything except the unreserved characters (letters, digits, hyphen, underscore, period, tilde). It encodes reserved characters including &, =, +, #, and /. Use encodeURIComponent when encoding a value that will be inserted into an existing URL as a query parameter value or path segment.

encodeURIComponent("hello world & more")
// Result: "hello%20world%20%26%20more"
// The & is encoded because it would break the query string if left raw

const q = encodeURIComponent(userInput);
const url = `https://example.com/search?q=${q}`;

The most common mistake is using encodeURI on a parameter value. It leaves & and = unencoded, which breaks the parameter structure. The correct pattern is always encodeURIComponent for values, encodeURI for complete URLs, and usually neither for anything you did not build yourself.

For most URL construction in modern JavaScript, URLSearchParams is the better choice over calling encodeURIComponent directly. Pass each value with params.set('key', value) and encoding happens automatically. Manual encodeURIComponent calls are correct but easy to apply inconsistently. One unencoded parameter in a URL string can break the entire query silently, with no error thrown at the point of the bug.

A URL shown in two states: a raw unencoded version with spaces and special characters highlighted in red, and the same URL percent-encoded below it with %20 and %26 sequences highlighted in green, shown against a dark terminal background

Which Characters Need Encoding? A Reference Table

This table shows the characters that most commonly cause bugs in URL handling, with their encoded form and the context where they appear:

CharacterEncoded FormWhy It Needs Encoding
Space%20Not allowed in URLs; breaks parsing everywhere
+%2BInterpreted as a space in form-encoded query strings
&%26Separates query parameters; breaks if in a value
=%3DSeparates parameter names from values
#%23Starts the fragment; everything after it is ignored by the server
%%25The encoding prefix itself must be encoded to appear literally
/%2FPath separator; changes path depth if unencoded in a segment
?%3FStarts the query string; breaks path parsing
@%40User info delimiter in authority component
:%3AScheme delimiter and port separator
,%2CSub-delimiter with context-dependent meaning
[ ]%5B %5DIPv6 address delimiters; illegal in most path contexts
"%22Not a URL character; breaks HTML attribute values
< >%3C %3ENot URL characters; cause HTML parsing problems
Non-ASCIIMulti-byte %XX%XXMust be UTF-8 encoded then percent-encoded

The characters that cause the most production bugs are #, +, and %. A # in a parameter value silently truncates the server-visible portion of the URL. An unencoded % followed by two hex digits gets interpreted as an encoding sequence. A + in a value gets decoded to a space by some servers and stays as a literal + by others, creating inconsistent behavior.

Space as %20 vs Space as +: Why Both Exist

This is the source of one of the most persistent URL encoding bugs, and it comes from two different standards that overlap.

RFC 3986 defines the URL standard. In this specification, a literal space in a URL must be encoded as %20. encodeURIComponent follows this standard.

The application/x-www-form-urlencoded format is an older format used for HTML form submission. In this format, spaces are encoded as + rather than %20. When you submit an HTML form with method="GET", the browser encodes the form values using this format, so spaces become + in the query string.

The two formats are not interchangeable. A server that expects %20 may or may not handle + correctly, and vice versa. When parsing + in a query string, PHP's $_GET superglobal decodes it as a space. JavaScript's URLSearchParams also decodes + as a space. But decodeURIComponent does not: it leaves + as a literal plus sign.

The correct conversion if you receive a form-encoded string and need to decode it in JavaScript:

// Replace + with %20 before calling decodeURIComponent
const decoded = decodeURIComponent(encodedValue.replace(/\+/g, '%20'));

Or use URLSearchParams which handles this automatically:

const params = new URLSearchParams(queryString);
const value = params.get('q'); // automatically decodes both %20 and +

How to Decode a URL-Encoded String

Decoding reverses the process: %XX sequences are replaced with their corresponding characters. In JavaScript, use decodeURIComponent() for component values and decodeURI() for full URLs.

Do not use unescape() even though it works for basic ASCII. It is deprecated, it does not handle multi-byte UTF-8 sequences correctly, and it treats + as a literal plus rather than a space.

For double-encoded URLs (where a percent sign was itself encoded, producing %2520 instead of %20), you need to decode twice:

decodeURIComponent(decodeURIComponent('%2520'))
// First decode: '%2520' → '%20'
// Second decode: '%20' → ' '

Double encoding happens when code encodes a value that is already encoded. Always check whether the input is already encoded before encoding again. A string that contains %20 is already encoded and should be decoded first, not encoded again.

Double encoding is a frequent source of bugs in systems that pass URL parameters through multiple layers: a frontend encodes a value, passes it to a backend route handler, which then encodes it again before forwarding it to a third-party API. The third-party API receives %2520 and either fails or returns a result for the literal string %20 rather than the intended space. If you are debugging an integration where parameter values arrive corrupted, check every layer in the chain and confirm which layer is responsible for encoding. Encoding should happen exactly once, as close to the point of URL construction as possible.

The # character deserves special attention in this context. Because # signals the start of the fragment identifier, anything after an unencoded # in a URL is silently dropped by the server before the request even reaches your code. The browser handles the fragment client-side and never sends it over the network. If a query parameter value contains # and it is not encoded as %23, the server receives a truncated URL with no error and no indication that part of the value was lost.

For working with other developer tools that handle encoded data, Base64 encoding is a separate encoding scheme used for binary data. Unlike percent encoding which is character-by-character, Base64 converts binary bytes to printable ASCII using a 64-character alphabet. The two systems appear together often in URLs that embed Base64-encoded data as query parameter values, which means the Base64 string itself must then be percent-encoded when placed in the URL. The URL Encode / Decode tool handles this outer layer. When you also work with API security, hash values commonly appear as URL-encoded signatures in webhook callbacks and signed URL schemes.

Frequently Asked Questions

URL encoding converts characters that are not allowed or have special meaning in a URL into a percent sign followed by two hexadecimal digits. A space becomes %20, an ampersand becomes %26, and a forward slash becomes %2F. The process is formally called percent encoding and is defined in RFC 3986. It ensures that any string can be safely transmitted as part of a URL without breaking the URL's structure or being misinterpreted by a server.

encodeURI encodes a complete URL and leaves characters that are valid URL structure characters unencoded: : / ? # [ ] @ ! $ & ' ( ) * + , ; = and the tilde. encodeURIComponent encodes a single URL component like a query parameter value or path segment and encodes all reserved characters including those that encodeURI leaves alone. Use encodeURIComponent when encoding a value that will be inserted into a URL. Use encodeURI when encoding a full URL string.

Both %20 and + represent a space, but in different contexts. The RFC 3986 standard for URLs specifies %20 for a literal space in any part of the URL. The older application/x-www-form-urlencoded format, used for HTML form submissions, replaces spaces with + signs. When you submit a form with GET method, spaces become + in the query string. When you construct a URL in JavaScript with encodeURIComponent, spaces become %20. Both are valid in their respective contexts but are not interchangeable.

Characters that must be encoded include: space (becomes %20), double quote (%22), less than (%3C), greater than (%3E), hash (#, if not a fragment delimiter, becomes %23), percent itself (%25), pipe (%7C), and curly braces (%7B %7D). Control characters and non-ASCII characters must also be encoded. Reserved characters like & = + # must be encoded when they appear as literal values in a query parameter rather than as URL syntax delimiters.

Use decodeURIComponent() to decode a percent-encoded string. For example, decodeURIComponent('Hello%20World') returns 'Hello World'. For a complete URL, use decodeURI() which only decodes characters that encodeURI would have encoded. Avoid using the older unescape() function, which is deprecated and does not handle UTF-8 characters correctly. If a URL-encoded value contains a + representing a space (form encoding), replace the + with %20 before calling decodeURIComponent, or replace all + with a space after decoding.

When a query parameter value contains characters like &, =, +, or #, those characters are interpreted as URL structure delimiters rather than literal values. An unencoded & in a value breaks it into what looks like two separate parameters. An unencoded # ends the query string and starts the fragment. Always encode query parameter values with encodeURIComponent before appending them to a URL. Never build URLs by concatenating raw user input directly into the query string.

HR

Written by

Hassaan Rasheed

Builder of ToolCenterHub. Passionate about creating fast, privacy-first tools that anyone can use without friction, accounts, or paywalls. Writing about design, development, and the web.

Connect on LinkedIn