querystring.unescape()

Decodes percent-encoded characters in a string without throwing on malformed sequences.

Signature
querystring.unescape(str)
  • Added in Node.js v0.1.25

Parameters

ParameterTypeDescription
$str<string>The string whose percent-encoded characters should be decoded.

Return value

<string>The decoded string.

Errors

Never throws for malformed percent-encoding: it tries the built-in decodeURIComponent() first and falls back to a safer equivalent that leaves invalid sequences in place.

querystring.unescape() is the decoder that querystring.parse() applies to every key and value. Like its counterpart querystring.escape(), it is exported mainly so it can be replaced — by assignment, or per call through parse()'s options.decodeURIComponent.

The reason to prefer it over decodeURIComponent()

It does not throw. decodeURIComponent('100%') raises a URIError, and since query strings arrive from clients you do not control, that error lands in a request handler as a 500 unless every call is wrapped in try. This function attempts the built-in first and, when that fails, falls back to a lenient decoder that leaves the broken sequence in the output and carries on. That is exactly why parse() survives hostile input.

What it does not do

It does not translate + into a space. parse() handles + separately before decoding, so decoding a form-encoded value by hand needs str.replace(/\+/g, ' ') first. Malformed bytes may decode to U+FFFD, so a value that survives is not necessarily the value that was sent — treat leniency as damage control, not as validation.

When to reach for it

Use it to decode a single value pulled out of a URL, a cookie, or a header, where an exception would be worse than an imperfect string.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Decoding one parameter you pulled out of a URL string by hand
  • Decoding a cookie or header value that may contain broken percent-encoding
  • Replacing decodeURIComponent in a request handler so bad input cannot throw a 500
  • Providing a custom decoder to querystring.parse() for a non-UTF-8 charset

Frequently asked questions

Why does querystring.unescape not throw on invalid percent-encoding?

By design: it tries decodeURIComponent() first and falls back to a decoder that leaves malformed sequences in place, so that querystring.parse() cannot be crashed by untrusted input. Use decodeURIComponent directly if you actually want an error on bad data.

Why is my '+' still there after querystring.unescape?

Because it only reverses percent-encoding, and the plus-means-space rule belongs to form encoding, which querystring.parse() applies before calling the decoder. Do str.replace(/\+/g, ' ') yourself before decoding, or use parse() on the whole pair.

Is querystring.unescape the same as decodeURIComponent?

For well-formed input, yes — the results are identical. They differ only on malformed input, where decodeURIComponent throws a URIError and this function returns a best-effort string that may contain U+FFFD replacement characters.

More examples

Decoding a form-encoded value by hand

The three lines isolate the single most reported surprise about this function. It is a percent-decoder only; the plus-for-space rule belongs to the form-encoding layer that parse() implements on top of it, so a hand-rolled decode must add that step.

Example · javascript
const querystring = require('node:querystring');

// A raw value taken straight out of 'note=hi+there%21'.
const raw = 'hi+there%21';

console.log(querystring.unescape(raw));
// hi+there!            <-- the '+' survived

// parse() replaces '+' before decoding; do the same yourself.
console.log(querystring.unescape(raw.replace(/\+/g, ' ')));
// hi there!

// Or just let parse() do both.
console.log(querystring.parse('note=' + raw).note);
// hi there!

Keeping a request handler alive on malformed input

Each of the three requests is one an attacker or a broken client will send. With decodeURIComponent the first and third crash the handler; the lenient fallback returns the undecodable text unchanged so the request completes with a sane response instead of an unhandled URIError.

Example · javascript
const http = require('node:http');
const querystring = require('node:querystring');

http.createServer((req, res) => {
  // A client can trivially send /?q=100%
  const raw = req.url.split('?')[1] ?? '';
  const value = raw.split('=')[1] ?? '';

  // decodeURIComponent(value) would throw URIError here -> 500.
  const safe = querystring.unescape(value);

  res.end(JSON.stringify({ q: safe }));
}).listen(3000);

// GET /?q=100%      -> {"q":"100%"}
// GET /?q=%E4%B8%AD -> {"q":"中"}
// GET /?q=%zz       -> {"q":"%zz"}

Swapping the decoder used by parse()

The two lengths show why a custom decoder is sometimes worth it: the default returns exactly the code points that were sent, so two visually identical names can differ byte-for-byte and fail an equality check or a database lookup. Reassigning querystring.unescape changes it for every consumer in the process, so keep that to application code.

Example · javascript
const querystring = require('node:querystring');

const builtin = querystring.unescape;

// A decoder that also normalises Unicode and strips control characters.
function strictUnescape(str) {
  return builtin(str).normalize('NFC').replace(/[-]/g, '');
}

// Per call:
console.log(
  querystring.parse('name=Cafe%CC%81', null, null, {
    decodeURIComponent: strictUnescape,
  }).name.length,
);
// 4   (composed to 'Café')

console.log(querystring.parse('name=Cafe%CC%81').name.length);
// 5   (decomposed 'e' + combining acute)

// Process-wide:
querystring.unescape = strictUnescape;

Discussion

  • Be the first to comment on this lesson.