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:
- A URL copied from the address bar — browsers display some characters decoded, so a pasted URL can contain a raw
%(or a literal space) thatdecodeURIComponentthen chokes on. - React Router / client-side query parsing — reading
location.searchand hand-decoding params throws on the first bad value. UseURLSearchParams(or the router's ownuseSearchParams) instead of manualdecodeURIComponent. - An API receiving an already-broken encoded string — a client that forgot to
encodeURIComponentsends?q=50% off; your server-side decode then throws. Guard it with atry/catchwrapper and return a 400 rather than crashing the handler. - Analytics / UTM tracking links — campaign URLs assembled by hand or in a spreadsheet often contain raw
%,&, or spaces, which throw when a tag manager or attribution script decodes them. - Truncated values — a cookie, a sliced query string, or a database column cut to a length limit can split a
%E2%82%ACsequence mid-escape, producing the "malformed URI sequence" variant.
Debugging checklist
- ✓ Is there a
%not followed by two hex digits? That's the cause — encode it as%25 - ✓ Was the value
encodeURIComponent-ed at the source? If not, encode there, not decode here - ✓ Decoding untrusted input? Wrap in
try/catchwith a fallback - ✓ Reading a query string? Use
URLSearchParamsinstead of manual splitting - ✓ "malformed URI sequence"? Check for a truncated multibyte UTF-8 escape
- ✓ Decoding more than once? Decode exactly once; match encode/decode counts
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.