URIError: URI malformed

Quick answer

decodeURIComponent throws URIError: URI malformed when the string has a lone % not followed by two hex digits (like 50% off), or an invalid/incomplete UTF-8 escape. Encode literal percents as %25 at the source, and wrap decoding of untrusted input in try/catch.

The exact error string

decodeURIComponent('100%');
// Uncaught URIError: URI malformed
//     at decodeURIComponent (<anonymous>)

decodeURIComponent('%E0%A4');   // truncated UTF-8 sequence
// Firefox: URIError: malformed URI sequence

Both decodeURIComponent and decodeURI throw this. The message never names the offending character, so the trick is knowing the two shapes that cause it: a stray %, or bytes that are not valid UTF-8.

The same problem is searched under several names — malformed URI sequence (Firefox's wording), a decodeURIComponent percent-encoding error, or a generic JavaScript URI decode error. They are all the one URIError described here.

Why a percent sign is special

In a URI, % is the escape character: %20 is a space, %2F is /. decodeURIComponent reads every % as "the next two characters are hex." When a % is followed by something that is not two hex digits — a space, a letter outside A–F, or the end of the string — the escape is malformed and it throws. A literal percent should have been encoded as %25 before it ever reached the string.

Cause 1: a literal % that was never encoded

This is the overwhelming majority of cases — user text, a price, or a search term with a percent sign goes into a URL un-encoded:

const term = '50% off';

// ❌ literal % placed straight into a URL, then decoded later
const url = '/search?q=' + term;            // /search?q=50% off
decodeURIComponent('50% off');              // URIError: URI malformed

// ✅ encode on the way in — '%' becomes '%25', ' ' becomes '%20'
const safe = '/search?q=' + encodeURIComponent(term);  // q=50%25%20off
decodeURIComponent('50%25%20off');          // '50% off'

Cause 2: invalid or truncated UTF-8

Each %xx can be valid hex yet still not form legal UTF-8 — for example a multibyte character cut off mid-sequence (a truncated query string, a sliced cookie). There is nothing to "fix" in the decoder; the bytes themselves are incomplete:

decodeURIComponent('%E2%82');     // € is %E2%82%AC — last byte missing
// URIError: URI malformed

decodeURIComponent('%E2%82%AC');  // ✅ '€'  (complete sequence)

Fix: a safe decode wrapper

When the input is untrusted (anything from the address bar, a form, or a third party), never let a stray % crash the page. Decode defensively:

function safeDecode(s) {
  try {
    return decodeURIComponent(s);
  } catch {
    return s;                 // not valid encoding — return as-is
  }
}

safeDecode('100%');           // '100%'   (no throw)
safeDecode('a%20b');          // 'a b'

Fix: prefer URLSearchParams for query strings

Most URIErrors come from hand-parsing query strings. URLSearchParams decodes values for you and removes the manual split/decodeURIComponent dance that introduces the bug:

// ❌ manual and fragile
const q = decodeURIComponent(location.search.split('q=')[1]);

// ✅ robust
const q2 = new URLSearchParams(location.search).get('q');

Real-world scenarios where this happens

The abstract "lone %" rarely appears on its own — here is where it actually bites in real apps:

Debugging checklist

Frequently Asked Questions

What does 'URIError: URI malformed' mean?

It means decodeURIComponent (or decodeURI) was given a string it cannot decode as valid percent-encoding. The usual trigger is a lone % that is not followed by two hexadecimal digits, like 100% or a%b, or an invalid/incomplete UTF-8 escape sequence.

Why does a percent sign break decodeURIComponent?

In a URI, % introduces a two-digit hex escape such as %20. decodeURIComponent treats every % that way, so a literal percent that was never encoded — 50% off — looks like the start of a broken escape and throws. The percent should have been encoded as %25 before it entered the string.

How do I decode untrusted input without throwing?

Wrap the call in try/catch and fall back to the original string: function safeDecode(s){ try { return decodeURIComponent(s); } catch { return s; } }. This prevents user-supplied values containing a stray % from crashing your code.

What causes 'malformed URI sequence'?

That is Firefox's wording for the same URIError. It also appears when the percent-escapes are individually valid but do not form valid UTF-8 — for example a truncated multibyte sequence like %E0%A4 with the third byte missing, or bytes that are not a legal UTF-8 encoding.

Should I use URLSearchParams instead?

For query strings, yes. new URLSearchParams(location.search).get('q') decodes values for you and is far more robust than splitting and calling decodeURIComponent by hand. It still cannot rescue genuinely invalid encoding, but it removes most of the manual mistakes that cause URIError.

Why does decoding twice throw?

If a value is decoded once, a sequence like %2520 becomes %20, and decoding again turns %20 into a space — but if the first decode produced a bare %, the second decode throws URIError. Decode exactly once; if you must encode for transport, encode and decode the same number of times on each side.

Encode or decode a URL component safely

Use the URL Encoder to percent-encode values (so % becomes %25) or decode a string and see exactly where it breaks — nothing is uploaded to a server.

URL Encoder / Decoder String Escape All Error References
About the author

Pasindu Ishan is a software developer based in Sri Lanka. He builds privacy-first developer tools at JSON Dev Tools.