URL Decode JavaScript β€” decodeURIComponent()

Β·Front-end & Node.js DeveloperΒ·Reviewed byMarcus WebbΒ·Published

Use the free online URL Decode Online directly in your browser β€” no install required.

Try URL Decode Online Online β†’

Percent-encoded strings appear in JavaScript code constantly β€” a search query arrives as q=standing+desk%26price%3A200, an OAuth redirect as next=https%3A%2F%2Fdashboard.internal%2F, a storage path as reports%2F2025%2Fq1.pdf. How to URL decode in JavaScript comes down to choosing the right one of three built-in functions: decodeURIComponent(), decodeURI(), and URLSearchParamsβ€” and the choice between them is the root cause of most silent data corruption I've seen in production codebases, particularly the +-as-space edge case and double-decoding. For a quick one-off decode without writing code, ToolDeck's URL Decoder handles it instantly in the browser. This JavaScript URL decoding tutorial covers all three functions in depth (ES2015+ / Node.js 10+): when to use each, how they differ for spaces and reserved characters, decoding from files and HTTP requests, safe error handling, and the four mistakes that cause the most subtle production bugs.

  • βœ“decodeURIComponent() decodes all percent-encoded sequences β€” it is the right choice for individual query parameter values and path segments
  • βœ“decodeURI() preserves structural URI characters like / ? & = # : β€” use it only when decoding a complete URL string, never for individual values
  • βœ“URLSearchParams.get() returns already-decoded values β€” calling decodeURIComponent() on top of it causes double-decoding
  • βœ“decodeURIComponent() does NOT decode + as a space β€” for form-encoded (application/x-www-form-urlencoded) data, use URLSearchParams or replace + before decoding
  • βœ“Always wrap decodeURIComponent() in try/catch when the input comes from user data β€” a bare % or incomplete sequence throws a URIError

What is URL Decoding?

Percent-encoding (formally defined in RFC 3986) replaces characters that are unsafe or structurally significant in a URL with a %sign followed by two hexadecimal digits β€” the character's UTF-8 byte value. URL decoding reverses this transformation: each %XX sequence is converted back to its original byte, and the resulting byte sequence is interpreted as UTF-8 text. A space decoded from %20, a slash from %2F, the non-ASCII character ΓΌ from %C3%BC (its two-byte UTF-8 representation).

The characters that are never encoded β€” called unreserved characters β€” are letters A–Z and a–z, digits 0–9, and - _ . ~. Everything else either has a structural role in the URL (like / separating path segments or & separating query parameters) or must be encoded when used as data. The practical result: a search filter like status=active&tier=premium encoded as a single query parameter value arrives looking nothing like the original.

Before Β· text
After Β· text
// Percent-encoded β€” as received in an HTTP request or webhook payload
"q=price%3A%5B200+TO+800%5D%20AND%20brand%3ANorthwood%20%26%20status%3Ain-stock"
// After URL decoding β€” the original Elasticsearch query filter
"q=price:[200 TO 800] AND brand:Northwood & status:in-stock"

decodeURIComponent() β€” The Standard Function for Decoding Values

decodeURIComponent() is the workhorse of URL decoding in JavaScript. It decodes every %XX sequence in the input string β€” including characters that have structural meaning in a URL, such as %2F (slash), %3F (question mark), %26 (ampersand), and %3D (equals sign). This makes it the correct choice for decoding individual query parameter values and path segments, but the wrong choice for decoding a complete URL β€” where those structural characters must remain encoded. It is a global function: no import is needed in any JavaScript environment.

Minimal working example

JavaScript (browser / Node.js)
// Decode individual query parameter values β€” the common case

const city     = decodeURIComponent('S%C3%A3o%20Paulo')          // 'SΓ£o Paulo'
const district = decodeURIComponent('It%C3%A1im%20Bibi')         // 'ItΓ‘im Bibi'
const category = decodeURIComponent('office%20furniture')         // 'office furniture'
const filter   = decodeURIComponent('price%3A%5B200+TO+800%5D')  // 'price:[200+TO+800]'
// Note: + is NOT decoded as space β€” see the + section in the comparison table

console.log(city)      // SΓ£o Paulo
console.log(district)  // ItΓ‘im Bibi
console.log(category)  // office furniture
console.log(filter)    // price:[200+TO+800]

Decoding a redirect URL extracted from a query parameter

JavaScript
// The redirect URL was percent-encoded when it was embedded as a parameter value
// receiving end: extract it, then use it β€” no manual decoding needed with URLSearchParams

const incomingUrl = 'https://auth.company.com/callback' +
  '?next=https%3A%2F%2Fdashboard.internal%2Freports%3Fview%3Dweekly%26team%3Dplatform' +
  '&session_id=sid_7x9p2k'

const url       = new URL(incomingUrl)
const rawNext   = url.searchParams.get('next')       // Auto-decoded by URLSearchParams
const sessionId = url.searchParams.get('session_id') // 'sid_7x9p2k'

// rawNext is already decoded: 'https://dashboard.internal/reports?view=weekly&team=platform'
// Do NOT call decodeURIComponent(rawNext) again β€” that would be double-decoding

const nextUrl = new URL(rawNext!)
console.log(nextUrl.hostname)                    // dashboard.internal
console.log(nextUrl.searchParams.get('view'))    // weekly
console.log(nextUrl.searchParams.get('team'))    // platform

Decoding non-ASCII and Unicode path segments

JavaScript
// REST API with internationalised path segments
// Each UTF-8 byte of the original character was percent-encoded separately

const encodedSegments = [
  '%E6%9D%B1%E4%BA%AC',   // 東京  (Tokyo)   β€” 3 bytes per character
  'M%C3%BCnchen',         // MΓΌnchen          β€” ΓΌ encoded as 2 bytes
  'caf%C3%A9',            // cafΓ©             β€” Γ© as precomposed NFC
  'S%C3%A3o%20Paulo',     // SΓ£o Paulo
]

encodedSegments.forEach(seg => {
  console.log(decodeURIComponent(seg))
})
// 東京
// MΓΌnchen
// cafΓ©
// SΓ£o Paulo

// Extracting a file key from a storage API URL
// The object key contained a / so it was encoded as %2F inside the path segment
const storageUrl = 'https://storage.api.example.com/v1/objects/reports%2F2025%2Fq1-financials.pdf'
const rawKey     = new URL(storageUrl).pathname.replace('/v1/objects/', '')
// .pathname decodes the URL-level encoding but %2F (as %252F at URL level) stays
// Use decodeURIComponent for the final step:
const fileKey = decodeURIComponent(rawKey)  // 'reports/2025/q1-financials.pdf'
console.log(fileKey)
Note:decodeURIComponent() throws a URIError when the input contains a % not followed by two valid hexadecimal digits β€” for example a bare % at the end of a string or a sequence like %GH. Always wrap it in try/catch when decoding user-supplied input. The safe decoding patterns are covered in the Error Handling section below.

JavaScript URL Decoding Functions β€” Character Reference

The three built-in decoding functions differ in exactly which encoded sequences they decode. The table shows the behaviour for the characters that matter most in practice:

EncodedCharacterdecodeURIComponent()decodeURI()URLSearchParams
%20spacespace βœ…space βœ…space βœ…
+plus (form)+ (kept)+ (kept)space βœ…
%2B+ literal+ βœ…+ βœ…+ βœ…
%26&& βœ…& (kept) ❌& βœ…
%3D== βœ…= (kept) ❌= βœ…
%3F?? βœ…? (kept) ❌? βœ…
%23## βœ…# (kept) ❌# βœ…
%2F// βœ…/ (kept) ❌/ βœ…
%3A:: βœ…: (kept) ❌: βœ…
%40@@ βœ…@ (kept) ❌@ βœ…
%25%% βœ…% βœ…% βœ…
%C3%BCΓΌΓΌ βœ…ΓΌ βœ…ΓΌ βœ…

The two critical rows are + and the structural characters (%26, %3D, %3F). URLSearchParams decodes + as a space because it follows the application/x-www-form-urlencoded specification β€” correct for HTML form submissions, but different from what decodeURIComponent() does with the same character. And decodeURI() silently skips %26, %3D, and %3F β€” which looks correct until a value actually contains an encoded ampersand or equals sign.

decodeURI() β€” Decoding a Complete URL Without Breaking Its Structure

decodeURI() is the counterpart to encodeURI(). It decodes a complete URL string while preserving characters that have structural meaning in a URI: ; , / ? : @ & = + $ #. These are left as their percent-encoded form (or as literal characters if they appeared unencoded in the input). This makes decodeURI() safe for sanitising full URLs that may contain non-ASCII characters in the path or hostname, without accidentally collapsing the query string structure.

Sanitising a URL with non-ASCII path segments

JavaScript
// A CDN URL with internationalised path segments and a structured query string
// decodeURI() decodes non-ASCII path but preserves ? & = intact

const encodedUrl =
  'https://cdn.example.com/assets/%E6%9D%B1%E4%BA%AC%2F2025%2Fq1-report.pdf' +
  '?token=eyJ0eXAiOiJKV1QiLCJhbGci&expires=1735689600'

const readable = decodeURI(encodedUrl)
console.log(readable)
// https://cdn.example.com/assets/東京/2025/q1-report.pdf?token=eyJ0eXAiOiJKV1QiLCJhbGci&expires=1735689600
// ↑ Non-ASCII decoded; ? & = preserved β€” URL remains structurally valid

// decodeURIComponent would destroy the URL β€” : / ? & = all decoded at once
const broken = decodeURIComponent(encodedUrl)
// 'https://cdn.example.com/assets/東京/2025/q1-report.pdf?token=eyJ0eXAiOiJKV1QiLCJhbGci&expires=1735689600'
// Looks the same here, but a URL like 'https%3A%2F%2F...' would be destroyed
Note:Prefer the URL constructor over decodeURI() when you also need to access individual URL components. new URL(str) normalises the input, validates its structure, and exposes .pathname, .searchParams, and .hostname as already-decoded properties. Reserve decodeURI() for cases where you only need a string result and cannot use the URL constructor (for example, in very old Node.js 6 environments without the global URL class).

URLSearchParams β€” Automatic Decoding for Query Strings

URLSearchParams is the idiomatic way to parse query strings in modern JavaScript. Every value returned by .get(), .getAll(), or iteration is automatically decoded β€” including + as a space for form-encoded data. It is available globally in all modern browsers and Node.js 10+, requires no import, and handles edge cases that manual string splitting gets wrong.

Parsing an incoming query string

JavaScript (browser / Node.js 10+)
// Parsing a webhook callback or OAuth redirect query string
const rawSearch =
  '?event_id=evt_9c2f4a1b' +
  '&product_name=Standing+Desk+Pro' +
  '&filter=price%3A%5B200+TO+800%5D' +
  '&tag=ergonomic&tag=adjustable' +
  '&redirect=https%3A%2F%2Fdashboard.internal%2Forders%3Fview%3Dpending'

const params = new URLSearchParams(rawSearch)

console.log(params.get('event_id'))      // 'evt_9c2f4a1b'
console.log(params.get('product_name'))  // 'Standing Desk Pro'       ← + decoded as space
console.log(params.get('filter'))        // 'price:[200+TO+800]'      ← %3A and %5B decoded
console.log(params.getAll('tag'))        // ['ergonomic', 'adjustable']
console.log(params.get('redirect'))      // 'https://dashboard.internal/orders?view=pending'

// Iterating all parameters
for (const [key, value] of params) {
  console.log(`${key}: ${value}`)
}
// event_id: evt_9c2f4a1b
// product_name: Standing Desk Pro
// filter: price:[200+TO+800]
// tag: ergonomic
// tag: adjustable
// redirect: https://dashboard.internal/orders?view=pending

Parsing query parameters from the current browser URL

JavaScript (browser)
// Current URL: https://app.example.com/search
//   ?q=standing+desk
//   &category=office+furniture
//   &sort=price_asc
//   &page=2

interface SearchFilters {
  query:    string | null
  category: string | null
  sort:     string
  page:     number
}

function getSearchFilters(): SearchFilters {
  const params = new URLSearchParams(window.location.search)
  return {
    query:    params.get('q'),                       // 'standing desk'
    category: params.get('category'),                // 'office furniture'
    sort:     params.get('sort') ?? 'relevance',
    page:     Number(params.get('page') ?? '1'),
  }
}

const filters = getSearchFilters()
console.log(filters.query)     // standing desk   (+ decoded automatically)
console.log(filters.category)  // office furniture

Decoding URL-Encoded Data from Files and API Responses

Two scenarios come up constantly in real projects: processing a file on disk that contains percent-encoded data (access logs, data exports, webhook capture files), and parsing the URL of an incoming HTTP request in a Node.js server. Both follow the same principle β€” use the URL constructor or URLSearchParams rather than manual string splitting β€” but the details differ.

Reading and decoding URL-encoded records from a file

JavaScript (Node.js 10+)
import { createReadStream } from 'fs'
import { createInterface } from 'readline'

// File: orders-export.txt β€” one URL-encoded record per line
// customer_name=Sarah+Chen&order_id=ord_9c2f4a&product=Standing+Desk+Pro&total=149.99%20EUR
// customer_name=Carlos+Mendoza&order_id=ord_7b3a1c&product=Ergonomic+Chair&total=89.00%20EUR
// customer_name=Yuki+Tanaka&order_id=ord_2e8d5f&product=Monitor+Arm&total=59.00%20EUR

interface Order {
  customerName: string | null
  orderId:      string | null
  product:      string | null
  total:        string | null
}

async function parseOrdersFile(filePath: string): Promise<Order[]> {
  const fileStream = createReadStream(filePath, { encoding: 'utf-8' })
  const rl         = createInterface({ input: fileStream, crlfDelay: Infinity })
  const orders: Order[] = []

  for await (const line of rl) {
    if (!line.trim()) continue

    // URLSearchParams decodes + as space and %XX sequences automatically
    const params = new URLSearchParams(line)

    orders.push({
      customerName: params.get('customer_name'),  // 'Sarah Chen'
      orderId:      params.get('order_id'),        // 'ord_9c2f4a'
      product:      params.get('product'),         // 'Standing Desk Pro'
      total:        params.get('total'),           // '149.99 EUR'
    })
  }

  return orders
}

const orders = await parseOrdersFile('./orders-export.txt')
console.log(orders[0])
// { customerName: 'Sarah Chen', orderId: 'ord_9c2f4a', product: 'Standing Desk Pro', total: '149.99 EUR' }

Parsing an Nginx access log to decode search queries

JavaScript (Node.js)
import { createReadStream } from 'fs'
import { createInterface } from 'readline'

// Nginx access log lines look like:
// 192.168.1.42 - - [11/Mar/2026:10:23:01 +0000] "GET /api/search?q=standing%20desk%26brand%3ANorthwood&sort=price_asc HTTP/1.1" 200 1842

async function extractSearchQueries(logFile: string): Promise<string[]> {
  const rl     = createInterface({ input: createReadStream(logFile), crlfDelay: Infinity })
  const queries: string[] = []

  for await (const line of rl) {
    // Extract the request path from the log line
    const match = line.match(/"GET ([^ ]+) HTTP/)
    if (!match) continue

    try {
      const requestUrl = new URL(match[1], 'http://localhost')
      const query      = requestUrl.searchParams.get('q')
      if (query) queries.push(query)  // URLSearchParams decodes automatically
    } catch {
      // Skip malformed lines β€” access logs may contain truncated entries
    }
  }

  return queries
}

const queries = await extractSearchQueries('/var/log/nginx/access.log')
console.log(queries)
// ['standing desk&brand:Northwood', 'ergonomic chair', 'monitor arm 27 inch']

Parsing query parameters in a Node.js HTTP server

JavaScript (Node.js 10+)
import http from 'http'

// Incoming URL: /api/products?q=standing+desk&warehouse=eu%2Dwest%2D1&minStock=10&cursor=eyJpZCI6MTIzfQ%3D%3D

const server = http.createServer((req, res) => {
  // Construct a full URL β€” the second arg is the base required by the URL constructor
  const requestUrl = new URL(req.url!, 'http://localhost')

  const searchQuery = requestUrl.searchParams.get('q')            // 'standing desk'
  const warehouseId = requestUrl.searchParams.get('warehouse')    // 'eu-west-1'
  const minStock    = Number(requestUrl.searchParams.get('minStock') ?? '0')
  const cursor      = requestUrl.searchParams.get('cursor')       // 'eyJpZCI6MTIzfQ=='

  if (!warehouseId) {
    res.writeHead(400, { 'Content-Type': 'application/json' })
    res.end(JSON.stringify({ error: 'warehouse parameter is required' }))
    return
  }

  const cursorData = cursor ? JSON.parse(Buffer.from(cursor, 'base64').toString()) : null

  res.writeHead(200, { 'Content-Type': 'application/json' })
  res.end(JSON.stringify({ searchQuery, warehouseId, minStock, cursorData }))
})

server.listen(3000)

When you need to inspect an encoded URL during development β€” to understand what a webhook is sending before writing the parsing code β€” paste it directly into ToolDeck's URL Decoder to see the decoded form instantly without running a script.

Command-Line URL Decoding

For shell scripts, CI pipelines, or quick one-off inspection of encoded strings, several approaches work without writing a full script. Node.js one-liners are cross-platform; on macOS and Linux, python3 is also always available.

bash
# ── Node.js one-liners ──────────────────────────────────────────────────

# Decode a single percent-encoded value
node -e "console.log(decodeURIComponent(process.argv[1]))" "S%C3%A3o%20Paulo%20%26%20Rio"
# SΓ£o Paulo & Rio

# Parse a query string and print each key=value pair (decoded)
node -e "
  const params = new URLSearchParams(process.argv[1])
  for (const [k, v] of params) console.log(`${k} = ${v}`)
" "q=standing+desk&warehouse=eu%2Dwest%2D1&minStock=10"
# q = standing desk
# warehouse = eu-west-1
# minStock = 10

# Decode and pretty-print a URL-encoded JSON body (common in webhook debugging)
node -e "
  const raw = decodeURIComponent(process.argv[1])
  console.log(JSON.stringify(JSON.parse(raw), null, 2))
" '%7B%22event%22%3A%22purchase%22%2C%22amount%22%3A149.99%2C%22currency%22%3A%22EUR%22%7D'
# {
#   "event": "purchase",
#   "amount": 149.99,
#   "currency": "EUR"
# }

# ── Python one-liner (available on most macOS/Linux systems) ─────────────
# unquote_plus also decodes + as space β€” correct for form-encoded data
python3 -c "
from urllib.parse import unquote_plus
import sys
print(unquote_plus(sys.argv[1]))
" "Standing+Desk+%26+Ergonomic+Chair"
# Standing Desk & Ergonomic Chair

# ── curl β€” log and decode a redirect URL from a response header ──────────
curl -sI "https://api.example.com/short/abc123" | grep -i location |   node -e "
    const line = require('fs').readFileSync('/dev/stdin', 'utf8')
    const url  = line.replace(/^location:s*/i, '').trim()
    console.log(decodeURIComponent(url))
  "

Graceful Decoding with decode-uri-component

The built-in decodeURIComponent() throws a URIError on any malformed sequence β€” including a trailing % without two following hex digits. In production code that processes user-supplied or third-party URLs β€” search query logs, clickthrough URLs from email campaigns, scraped web data β€” malformed sequences are common enough to cause crashes. The decode-uri-component package (~30M weekly npm downloads) handles malformed sequences gracefully by returning the original sequence unchanged instead of throwing.

bash
npm install decode-uri-component
# or
pnpm add decode-uri-component
JavaScript (Node.js)
import decodeUriComponent from 'decode-uri-component'

// Native function throws on malformed input β€” can crash a request handler
try {
  decodeURIComponent('product%name%')   // ❌ URIError: URI malformed
} catch (e) {
  console.error('Native threw:', (e as Error).message)
}

// decode-uri-component returns the raw sequence instead of throwing
console.log(decodeUriComponent('product%name%'))
// product%name%   ← malformed sequences left as-is, no throw

// Valid sequences are decoded correctly
console.log(decodeUriComponent('S%C3%A3o%20Paulo%20%26%20Rio'))
// SΓ£o Paulo & Rio

// Mixed: valid sequences decoded, invalid ones preserved
console.log(decodeUriComponent('Berlin%20Office%20%ZZ%20HQ'))
// Berlin Office %ZZ HQ   ← %ZZ is not valid hex β€” kept as-is

// Practical use in a log processing pipeline
const rawSearchQueries = [
  'standing%20desk%20ergonomic',
  'price%3A%5B200+TO+800%5D',
  '50%25+off',       // ← would throw with decodeURIComponent  (bare %)
  'wireless%20keyboard%20%26%20mouse',
]

const decoded = rawSearchQueries.map(q => decodeUriComponent(q))
console.log(decoded)
// ['standing desk ergonomic', 'price:[200+TO+800]', '50%25+off', 'wireless keyboard & mouse']

Use decodeURIComponent() with a try/catch for application code where you want to know immediately if unexpected input arrives. Reach for decode-uri-component in data pipelines, log processors, and webhook handlers where you want to continue processing even when some inputs contain invalid encoding.

Handling Malformed Percent-Encoded Strings

A bare %, an incomplete sequence like %A, or an invalid pair like %GH all cause decodeURIComponent() to throw URIError: URI malformed. Any user-controlled input β€” search queries, URL fragments, form fields containing URLs, parameters from email campaign links β€” can contain these sequences. A safe wrapper is essential for any externally-facing code.

Safe decode wrappers for common scenarios

JavaScript
// ── 1. Basic safe decode β€” returns original string on error ─────────────
function safeDecode(encoded: string): string {
  try {
    return decodeURIComponent(encoded)
  } catch {
    return encoded  // return raw input if decoding fails β€” never crash
  }
}

// ── 2. Safe decode + handle + as space (for form-encoded values) ─────────
function safeFormDecode(formEncoded: string): string {
  try {
    return decodeURIComponent(formEncoded.replace(/+/g, ' '))
  } catch {
    return formEncoded.replace(/+/g, ' ')  // at least replace + even if rest fails
  }
}

// ── 3. Safe full query string parser β†’ plain object ──────────────────────
function parseQueryString(queryString: string): Record<string, string> {
  const result: Record<string, string> = {}
  try {
    const params = new URLSearchParams(queryString)
    for (const [key, value] of params) {
      result[key] = value  // URLSearchParams handles all decoding internally
    }
  } catch {
    // Silently return empty on completely malformed input
  }
  return result
}

// Usage
console.log(safeDecode('S%C3%A3o%20Paulo'))         // SΓ£o Paulo
console.log(safeDecode('search%20for%2050%25+off'))  // search for 50% off (bare %)
                                                     // β†’ actually fine; % here is %25
console.log(safeDecode('malformed%string%'))         // malformed%string%  (no throw)

console.log(safeFormDecode('standing+desk+pro'))     // standing desk pro

console.log(parseQueryString('q=hello+world&tag=node%20js&page=2'))
// { q: 'hello world', tag: 'node js', page: '2' }

Common Mistakes

I've seen these four patterns cause silent data corruption or unexpected crashes in production β€” usually only when a value happens to contain a special character, which means they slip through unit tests and surface with real user data.

Mistake 1 β€” Double-decoding a URLSearchParams value

Problem: URLSearchParams.get() returns an already-decoded string. Calling decodeURIComponent() on top of it double-decodes the value β€” turning any remaining % in the decoded output into %25, which corrupts the data. Fix: use the value from URLSearchParams.get() directly β€” no additional decoding is needed.

Before Β· JavaScript
After Β· JavaScript
// ❌ URLSearchParams already decoded β€” decoding again corrupts values with %
const qs        = new URLSearchParams('rate=50%25&redirect=https%3A%2F%2Fdashboard.internal%2F')
const rawRate   = qs.get('rate')       // '50%'   ← already decoded
const wrongRate = decodeURIComponent(rawRate)
// '50%25'  ← % was decoded to %25 on the second pass β€” now wrong again
// βœ… Use URLSearchParams.get() directly β€” decoding is automatic
const qs       = new URLSearchParams('rate=50%25&redirect=https%3A%2F%2Fdashboard.internal%2F')
const rate     = qs.get('rate')        // '50%'   ← correct, no extra step needed
const redirect = qs.get('redirect')    // 'https://dashboard.internal/'
const parsed   = new URL(redirect!)    // Safe to construct β€” already decoded
console.log(parsed.hostname)           // dashboard.internal

Mistake 2 β€” Not decoding + as a space for form-encoded data

Problem: Form submissions and some OAuth libraries encode spaces as + (application/x-www-form-urlencoded). Calling decodeURIComponent() on this data leaves + as a literal plus sign. Fix: use URLSearchParams to parse form-encoded data, or replace + with a space before calling decodeURIComponent().

Before Β· JavaScript
After Β· JavaScript
// ❌ decodeURIComponent treats + as a literal plus, not a space
// OAuth token endpoint sends: grant_type=authorization_code&code=SplxlOBeZQQYb...
//   &redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback
// Some legacy OAuth implementations also use + for spaces in codes
const formBody   = 'customer_name=Sarah+Chen&product=Standing+Desk+Pro&qty=2'
const [, rawVal] = formBody.split('&')[0].split('=')
const name       = decodeURIComponent(rawVal)
console.log(name)  // 'Sarah+Chen'  ← + not converted to space
// βœ… Use URLSearchParams β€” follows application/x-www-form-urlencoded spec
const formBody   = 'customer_name=Sarah+Chen&product=Standing+Desk+Pro&qty=2'
const params     = new URLSearchParams(formBody)
console.log(params.get('customer_name'))  // 'Sarah Chen'   ← + correctly decoded as space
console.log(params.get('product'))        // 'Standing Desk Pro'

Mistake 3 β€” Using decodeURI() for individual query parameter values

Problem: decodeURI() does not decode %26 (&), %3D (=), or %3F (?) β€” characters commonly encoded inside parameter values. Using it to decode a single value leaves those sequences intact, silently producing incorrect output. Fix: use decodeURIComponent() for individual values; reserve decodeURI() for full URL strings.

Before Β· JavaScript
After Β· JavaScript
// ❌ decodeURI does not decode & and = β€” the value stays broken
const encodedFilter = 'status%3Dactive%26tier%3Dpremium%26region%3Deu-west'
const filter        = decodeURI(encodedFilter)
console.log(filter)
// 'status%3Dactive%26tier%3Dpremium%26region%3Deu-west'  ← %3D and %26 not decoded
// βœ… decodeURIComponent decodes all sequences including & and =
const encodedFilter = 'status%3Dactive%26tier%3Dpremium%26region%3Deu-west'
const filter        = decodeURIComponent(encodedFilter)
console.log(filter)
// 'status=active&tier=premium&region=eu-west'  ← correctly decoded

Mistake 4 β€” Not wrapping decodeURIComponent in try/catch for user input

Problem: A bare %not followed by two hex digits β€” common in search queries typed by users (β€œ50% off”, β€œ100% cotton”) β€” causes decodeURIComponent() to throw URIError: URI malformed, crashing the request handler if uncaught. Fix: always wrap in try/catch when input originates from user data, URL fragments, or external systems.

Before Β· JavaScript
After Β· JavaScript
// ❌ User typed '100% cotton' in a search box β€” bare % crashes the server
// GET /api/search?q=100%25+cotton  ← This specific case is fine (%25 = %)
// GET /api/search?q=100%+cotton    ← This crashes (% not followed by 2 hex digits)
app.get('/api/search', (req, res) => {
  const query = decodeURIComponent(req.query.q as string)
  // ↑ throws URIError: URI malformed for '100% cotton'  β†’ unhandled 500 error
})
// βœ… Wrap in try/catch β€” fall back to raw input if decoding fails
app.get('/api/search', (req, res) => {
  let query: string
  try {
    query = decodeURIComponent(req.query.q as string)
  } catch {
    query = req.query.q as string  // use the raw value rather than crashing
  }
  // continue processing safely β€” query is either decoded or raw
})

decodeURIComponent vs decodeURI vs URLSearchParams β€” Quick Comparison

MethodDecodes %XXDecodes + as spaceThrows on malformedDecodes & = ? #Use caseRequires install
decodeURIComponent()βœ… all❌ noβœ… URIErrorβœ… yesIndividual values and path segmentsNo
decodeURI()βœ… most❌ noβœ… URIError❌ noComplete URL stringsNo
URLSearchParamsβœ… allβœ… yes❌ silentβœ… yesQuery string parsing with automatic decodeNo
URL constructorβœ… allβœ… yesβœ… TypeErrorβœ… yesFull URL parsing and normalisationNo
decode-uri-componentβœ… all❌ no❌ silentβœ… yesBatch decode with malformed-input tolerancenpm install
querystring.unescape()βœ… all❌ no❌ silentβœ… yesLegacy Node.js (deprecated in v16)No (built-in)

For the vast majority of cases, the choice reduces to three scenarios. Use URLSearchParams to parse query strings β€” it handles decoding, the +-as-space rule, and repeated keys automatically. Use decodeURIComponent() (wrapped in try/catch) for a single value or path segment, especially when you expect %2F-encoded slashes inside a segment. Use decodeURI() only when you have a complete URL string with non-ASCII characters in its path and need the structural characters (/ ? & =) to remain encoded in the output.

Frequently Asked Questions

What is the difference between decodeURIComponent() and decodeURI() in JavaScript?
decodeURIComponent() decodes every percent-encoded sequence in the string, including characters that have structural meaning in a URL β€” & (%26), = (%3D), ? (%3F), # (%23), / (%2F), and : (%3A). It is designed for decoding individual query parameter values or path segments. decodeURI() preserves those structural characters β€” it does not decode %26, %3D, %3F, %23, %2F, or %3A β€” because it is designed to sanitise a complete URL without breaking its query string structure. Using decodeURI() on a single parameter value that contains an encoded & or = will silently leave those sequences undecoded, producing wrong output.
Why does decodeURIComponent() throw a URIError for some strings?
decodeURIComponent() throws URIError: URI malformed when the input contains a % not followed by exactly two valid hexadecimal digits. Common triggers: a bare % at the end of a string ("50% off" typed by a user), an incomplete sequence ("%A"), or a non-hex pair ("%GH"). This happens most often with user-typed search queries or values pasted from text that was never meant to be URL-encoded. The fix is to wrap decodeURIComponent() in a try/catch block and return the raw string on error. The decode-uri-component npm package offers the same fix without requiring a try/catch wrapper.
Does URLSearchParams automatically decode percent-encoded values?
Yes. Every value returned by URLSearchParams.get() and URLSearchParams.getAll() is fully decoded β€” you should never call decodeURIComponent() on their output. URLSearchParams also follows the application/x-www-form-urlencoded specification, which decodes + as a space, making it correct for HTML form submissions and OAuth token responses. The only case where URLSearchParams cannot help is decoding a standalone encoded value that is not part of a query string β€” for that, use decodeURIComponent() with a try/catch.
How do I decode the + sign as a space in JavaScript?
decodeURIComponent() treats + as a literal plus sign β€” it does not convert + to a space. This is intentional: + as space is an application/x-www-form-urlencoded convention, separate from the percent-encoding standard. To decode + as a space, use URLSearchParams (which follows the form-encoded spec), or replace + before calling decodeURIComponent(): decodeURIComponent(str.replace(/\+/g, ' ')). Note that replacing + with %20 before decoding also works but is slightly more verbose. Always prefer URLSearchParams for parsing full query strings β€” it handles both %20 and + correctly.
How do I URL decode a string in Node.js?
Use the same global functions available in all browsers β€” no import is needed in Node.js 10+. decodeURIComponent() for individual values, decodeURI() for complete URL strings, URLSearchParams for query strings. For incoming HTTP request URLs, use new URL(req.url, 'http://localhost') β€” the URL constructor normalises the URL and exposes .searchParams (auto-decoded) and .pathname (decoded at the URL level). The older querystring.unescape() function still exists but is deprecated since Node.js 16 β€” prefer decodeURIComponent() instead.
How do I parse a URL query string into an object in JavaScript?
Use URLSearchParams with Object.fromEntries(): const params = Object.fromEntries(new URLSearchParams(queryString)). This gives a plain object with all parameter keys and decoded values. Note that Object.fromEntries() keeps only the last value for duplicate keys β€” for repeated keys like tag=one&tag=two, use URLSearchParams.getAll('tag') instead. For the browser, new URLSearchParams(window.location.search) parses the current page's query string automatically. For Node.js, parse from new URL(req.url, base).searchParams.

For a one-click decode without writing any code, paste your percent-encoded string directly into ToolDeck's URL Decoder β€” it decodes instantly in the browser, with the result ready to copy into your code or terminal.

AC
Alex ChenFront-end & Node.js Developer

Alex is a front-end and Node.js developer with extensive experience building web applications and developer tooling. He is passionate about web standards, browser APIs, and the JavaScript ecosystem. In his spare time he contributes to open-source projects and writes about modern JavaScript patterns, performance optimisation, and everything related to the web platform.

MW
Marcus WebbTechnical Reviewer

Marcus specialises in JavaScript performance, build tooling, and the inner workings of the V8 engine. He has spent years profiling and optimising React applications, working on bundler configurations, and squeezing every millisecond out of critical rendering paths. He writes about Core Web Vitals, JavaScript memory management, and the tools developers reach for when performance really matters.