Short answer: use %20 when expressing the UTF-8/ASCII space byte through percent-encoding. Expect + to mean a space only in application/x-www-form-urlencoded names and values, including URLSearchParams. Encode a literal plus as %2B in that format. Never apply form decoding indiscriminately to an entire URL.

%20 is the percent-encoded space byte

ASCII and UTF-8 represent U+0020 SPACE as byte hexadecimal 20. RFC 3986 percent-encoding writes one byte as a percent sign and two hexadecimal digits, so the result is %20. This relationship does not depend on whether the byte appears in a path, query, or fragment; whether a space is allowed or how it should be serialized still depends on that component.

The literal plus character U+002B is a different byte, hexadecimal 2B. Its percent escape is %2B. A generic percent decoder therefore maps %20 to space and %2B to plus. It does not need a rule that raw plus means space.

The plus rule belongs to application/x-www-form-urlencoded

The WHATWG URL Standard's form-urlencoded format serializes ordered name/value tuples. During serialization, its percent-encode operation uses a spaceAsPlus flag: byte 20 is emitted as +. During parsing, every raw plus byte in a name or value is replaced with a space before percent-decoding.

This historical form behavior is why HTML form submissions and many server query parsers accept q=red+blue as the value “red blue.” It does not redefine plus throughout every URL component. RFC 3986 lists plus among reserved sub-delimiters; a scheme or application can give it meaning, but the generic URI syntax does not call it a space escape.

Input text in a form valueSerialized bytesParsed value
red bluered+blue or compatible red%20bluered blue
C++C%2B%2BC++
a+b ca%2Bb+ca+b c

A plus in a path stays a plus

Consider https://example.com/docs/C++. Under browser URL path processing, the two plus characters remain literal plus characters; they do not become spaces merely because the address contains a query elsewhere. Replacing plus globally would corrupt the path. The same caution applies to fragments, signatures, Base64 text, and opaque identifiers.

Component-sensitive interpretation
/docs/a+b                 # path contains a literal plus
?q=a+b                    # form-style q value parses as "a b"
?q=a%2Bb                  # form-style q value parses as "a+b"
#a+b                      # fragment text contains a literal plus

A backend framework may apply its own decoding policy to route parameters, so document and test that layer. Do not infer a standards rule from one framework's convenience behavior. The URL Parser separates the path, query, and fragment so the boundary is visible without contacting the destination.

A query is not automatically a form

RFC 3986 defines the query component's allowed syntax but does not require all queries to be name/value tuples or give plus a universal space meaning. Search APIs commonly choose form-style pairs, but another protocol may treat the raw query as one opaque string, use semicolons, preserve order and duplicates specially, or define a signature over exact bytes.

Use URLSearchParams only when the query contract is compatible with its form-urlencoded model. It supports ordered pairs and duplicate names, and it converts plus to space while parsing. If a signature protocol defines a different canonical query algorithm, follow that protocol rather than parsing and reserializing with a generic helper.

URL.search and URLSearchParams can serialize differently

The WHATWG URL Standard notes that a URL's direct query serializer and URLSearchParams use different percent-encode sets. A URL may expose an existing space as %20; after changing or sorting searchParams, the form serializer can emit it as +. The decoded name/value may remain the same while the exact URL string changes.

Equivalent value, different spelling
const url = new URL("https://example.com/?q=red%20blue");
url.searchParams.get("q"); // "red blue"

url.searchParams.sort();
url.search;                // may be "?q=red+blue"

That difference matters for cache keys, HMAC inputs, presigned URLs, deduplication, and tests that compare raw strings. Decide whether your contract compares decoded pairs or the original serialization. Do not normalize a signed URL after the signature was computed.

encodeURIComponent does not emit form-style plus

encodeURIComponent("red blue") returns red%20blue. It encodes a plus in input as %2B, which makes it useful for one manually composed component. URLSearchParams given the same text returns q=red+blue. This is an algorithm difference, not a browser inconsistency.

For new code, prefer assigning values through URLSearchParams.set or append instead of manually concatenating & and =. The encodeURI versus encodeURIComponent guide covers the remaining reserved characters and the historical JavaScript unescaped set.

Decode in the correct order and exactly once

A form-urlencoded parser first splits the byte sequence into fields, replaces raw plus with space within each name and value, then percent-decodes and UTF-8 decodes them. Blindly calling decodeURIComponent on the whole query does not implement all of those steps: it leaves raw plus untouched and can decode an escaped ampersand before a later split.

Do not run a second decode because the output still contains %. An originally literal %2B can be transported as %252B; one layer may own the outer escape. A second unplanned decode changes data to a plus and a later form pass can change it again to a space. See double URL encoding for the security consequences.

A safe diagnostic workflow

  1. Keep the raw URL. Record the exact path, query, and fragment before transforming it.
  2. Identify the contract. Confirm whether the query is form-urlencoded or another format.
  3. Parse structure once. Split the complete URL before decoding component data.
  4. Preserve duplicates and order. They may be semantically or cryptographically significant.
  5. Decode one round. Reject malformed escapes and invalid UTF-8 rather than guessing.
  6. Compare decoded and serialized forms. A value can remain equal while its raw spelling changes.

The Query String Parser, URL Encoder, URL Decoder, and URL Parser perform these inspections in the browser without fetching the address or sending entered text to a processing API. General text input is limited to 200,000 UTF-16 code units; complete URL and base fields are limited to 32,768.

Primary specifications

Debugging a lost plus sign? Parse the query as ordered form-style pairs, then compare the raw field with the decoded value.

Open the Query String Parser

Frequently asked questions

Is %20 or + the correct encoding for a space?

Both can represent a space in the right context. %20 is the percent-encoded space byte. Plus represents space only in application/x-www-form-urlencoded parsing and serialization, including URLSearchParams.

Does plus mean space in a URL path?

No. The general URL path rules do not translate plus to space. A path segment containing a literal plus should remain plus unless the application defines an additional nonstandard decoding rule.

How do I put a literal plus sign in a query parameter?

In an application/x-www-form-urlencoded query value, encode the literal plus byte as %2B. URLSearchParams does this automatically when given a value such as C++.

Why does URLSearchParams convert spaces to plus signs?

URLSearchParams uses the WHATWG application/x-www-form-urlencoded serializer. That algorithm sets spaceAsPlus and emits the space byte as a plus sign for form compatibility.

Can I replace every plus sign with a space before decoding?

No. Perform that replacement only inside a known application/x-www-form-urlencoded name or value. A blanket replacement can corrupt literal plus signs in paths, fragments, opaque data, or query formats with different rules.

Does LiveParse fetch a URL when parsing its query?

No. Query and URL parsing run locally in the browser without requesting the destination. General text is limited to 200,000 UTF-16 code units, while complete URL and base URL inputs are limited to 32,768.