DevToolNow

URL Encoding Explained: When to Use encodeURI vs encodeURIComponent

DevToolNow Editorial Team···10 min read

URL encoding looks like a solved problem until you watch a senior engineer chase a bug that turns "a+b@example.com" into "a b@example.com" somewhere in a redirect chain. The rules are precise — they live in RFC 3986 and the WHATWG URL Standard — but JavaScript exposes three different encoders with overlapping responsibilities, and the form-urlencoded variant adds a fourth set of rules on top. This guide separates them, names the bugs they cause, and explains why URLSearchParams is usually the right answer.

1. Percent-encoding and the RFC 3986 character classes

RFC 3986 — the URI specification — divides characters into three classes. Unreserved characters (A–Z a–z 0–9 - _ . ~) appear literally and never need encoding. Reserved characters (: / ? # [ ] @ ! $ & ' ( ) * + , ; =) have structural meaning and must be encoded when they appear in data rather than structure. Everything else (spaces, non-ASCII, control characters) must always be percent-encoded.

Percent-encoding represents a byte as % followed by two hex digits — %20 for a space, %2F for a forward slash, %E4%B8%AD for the UTF-8 encoding of 中. The encoding is byte-level; for non-ASCII characters you encode the UTF-8 bytes, not the Unicode code point.

2. encodeURI vs encodeURIComponent

JavaScript ships two global encoders that look similar and behave very differently:

  • encodeURI(s) assumes s is a complete URL and only encodes characters that should never appear in a URL at all. It deliberately leaves reserved characters (: / ? # & =) untouched because they have structural meaning.
  • encodeURIComponent(s) assumes s is data destined for one slot in a URL — a path segment, a query value, a hash fragment — and encodes every reserved character so the result is safe to drop into the URL without breaking structure.

The pragmatic rule: when you control the structure and are filling in slots, use encodeURIComponent on each slot. encodeURI is appropriate only when you have an entire URL string that may contain a stray space and you want a quick fix-up — and even then, you are usually better off parsing it with new URL().

3. URLSearchParams: the right tool for query strings

For query strings specifically, URLSearchParams is almost always the right tool. It handles encoding, decoding, repeated keys, and the form-urlencoded rules automatically:

const params = new URLSearchParams();
params.set("q", "hello world");
params.set("tag", "a+b");
url.search = params.toString();
// "q=hello+world&tag=a%2Bb"

Note the two distinct treatments of +: the literal space in "hello world" is encoded as + (form-urlencoded behaviour), while the + inside the value "a+b" is encoded as %2B so it round-trips correctly. Manual encodeURIComponent("hello world") would produce hello%20world instead — both are valid, but consistency with form decoding requires the + form.

4. Path encoding vs query encoding

Path segments and query values do not have the same rules. In a path, / is structural, so a slash inside a path-segment value must be encoded as %2F. In a query, / is allowed literally and many APIs do not bother encoding it. + in a path is a literal plus sign per RFC 3986 strict parsers; + in a query is conventionally a space because of form-urlencoded.

The cleanest path through this is structural rather than character-level: build URLs through the URL constructor and assign to .pathname, .searchParams, etc. The constructor applies the right encoding for each component automatically:

const u = new URL("https://api.example.com/");
u.pathname = `/users/${encodeURIComponent(userId)}/items`;
u.searchParams.set("q", searchTerm);
fetch(u);

5. Double encoding bugs

Double encoding is the single most common URL bug. It happens when an already-encoded string is run through an encoder again: a literal space starts as " ", becomes "%20" after one pass, then "%2520" on the second pass because the % is itself a reserved character. The result is a URL that decodes to "%20" instead of a space.

Three common sources of the bug: encoding a value before passing it to a library that also encodes; encoding a full URL (running encodeURIComponent on a complete URL escapes the slashes and colons); and decoding a value, modifying it, and forgetting to re-encode before reassembling. Track the encoding state of every URL string in your call graph the same way you track string-vs-bytes in low-level languages.

6. application/x-www-form-urlencoded

The form-urlencoded format is a sibling of URL query encoding with two key differences: spaces are + rather than %20, and the encoding is applied to a key/value list rather than a single string. It is the default body format for <form method="post"> submissions, the default for fetch with a URLSearchParams body, and what most server frameworks decode by default for Content-Type: application/x-www-form-urlencoded requests.

On the wire, the + rule applies in both query strings (because forms historically used GET and put their data in the query) and request bodies. The WHATWG URL Standard documents the form-urlencoded variant separately as the "application/x-www-form-urlencoded" serializer.

7. Internationalised URLs and Punycode

Path and query components encode non-ASCII as percent-encoded UTF-8 bytes. Hostnames are different: RFC 3986 only allows ASCII in the host. Internationalised domain names like 例え.jp are converted to ASCII through Punycode (RFC 3492), producing labels prefixed with xn--. Browsers display the Unicode form to humans but transmit the Punycode form. IDNA 2008 (RFC 5891–5894) and the Unicode UTS #46 mapping define which characters are valid in IDN labels.

For URL libraries, this is mostly invisible — pass the Unicode form into new URL() and read .host to get the Punycode form. For raw HTTP libraries that do not normalise the hostname, you have to call your platform's IDN library yourself.

8. Decision tree

  • Building a query string → URLSearchParams.
  • Building a complete URL → new URL(), then assign to .pathname and .searchParams.
  • Filling a single value into a path or fragment → encodeURIComponent.
  • Already have a complete URL with a stray space → Re-parse with new URL() if possible; encodeURI as a last resort.
  • Reading a query value out of a URL → url.searchParams.get("name") — never split on &/= manually.
  • Internationalised host → Let new URL() handle Punycode for you.

Encode and decode URLs in your browser

DevToolNow's URL Encoder runs all four common encoding modes — encodeURI, encodeURIComponent, form-urlencoded, and percent-encoding — locally, with side-by-side output. Useful for debugging double-encoding bugs.

Open URL Encoder →

Frequently asked questions

Q. Should I use encodeURI or encodeURIComponent?

A. Almost always encodeURIComponent. Use it on each component (path segment, query value, fragment) before assembling them into a URL. encodeURI is for the rare case of fixing up a complete URL string that may contain characters outside the URI character set, like an unencoded space — and even then, building the URL from a URL or URLSearchParams object is safer than running encodeURI on a concatenated string.

Q. Why does %20 sometimes become +?

A. In application/x-www-form-urlencoded — the encoding used by HTML form submissions and the body of POST forms — spaces are encoded as the literal + character, not %20. In path or query of an RFC 3986 URI, spaces are %20. URLSearchParams emits + for spaces because it follows the form-urlencoded rules. Server-side decoders that follow RFC 3986 strictly treat + as a literal plus sign in the path; in the query, most accept both — but assume nothing.

Q. What is double encoding and how do I avoid it?

A. Double encoding is when an already-encoded value is encoded again — "hello world" becomes "hello%20world", then "hello%2520world". The %25 is the encoding of the % from the first pass. The fix is to track the encoding state of every string you handle: never encode a value that came from a URL parser already, and never decode a value that you immediately want to put back into a URL. The URL and URLSearchParams APIs handle this automatically; manual concatenation does not.

Q. How do international domain names get encoded?

A. RFC 3986 only allows ASCII in the host part of a URL, so internationalised domain names (IDNs) like 例え.jp are converted to ASCII via Punycode (RFC 3492) before transmission, becoming xn--r8jz45g.jp. Browsers display the Unicode form to humans but send the Punycode form on the wire. The IDNA 2008 / IDNA 2003 specifications define the full rules including which Unicode characters are allowed.

Q. Is the URL constructor safe to use on user input?

A. Yes. new URL(input) parses according to the WHATWG URL Standard — the spec that governs how browsers handle URLs in 2026 — and either returns a URL object or throws on invalid input. It applies the correct encoding for each part of the URL when you read them back through .pathname, .search, .href, etc. Use it as your default URL builder; reserve manual encodeURIComponent for cases where you need RFC 3986 strictness rather than WHATWG.

References

  • IETF RFC 3986 — Uniform Resource Identifier (URI): Generic Syntax
  • IETF RFC 1738 — Uniform Resource Locators (URL) (historical)
  • WHATWG — URL Standard (the spec browsers actually implement)
  • MDN Web Docs — encodeURIComponent, URL, URLSearchParams
  • IETF RFC 3492 — Punycode: A Bootstring encoding of Unicode

Note: URL handling differs subtly between the strict RFC 3986 parsers used by some backends and the WHATWG URL Standard implemented by browsers. When integrating across this boundary, validate round-trip behaviour with representative payloads.

About the DevToolNow Editorial Team

DevToolNow's editorial team is made up of working software developers who use these tools every day. Every guide is reviewed against primary sources — IETF RFCs, W3C/WHATWG specifications, MDN Web Docs, and project repositories on GitHub — before publication. We update articles when standards change so the guidance stays current.

Sources we cite: IETF RFCs · MDN Web Docs · WHATWG · ECMAScript spec · Official project READMEs on GitHub