URL Decode JavaScript β decodeURIComponent()
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.
// 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
// 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
// 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')) // platformDecoding non-ASCII and Unicode path segments
// 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)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:
| Encoded | Character | decodeURIComponent() | decodeURI() | URLSearchParams |
|---|---|---|---|---|
| %20 | space | space β | 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
// 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
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
// 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=pendingParsing query parameters from the current browser URL
// 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 furnitureDecoding 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
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
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
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.
# ββ 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.
npm install decode-uri-component # or pnpm add decode-uri-component
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
// ββ 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.
// β 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.internalMistake 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().
// β 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.
// β 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®ion=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.
// β 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
| Method | Decodes %XX | Decodes + as space | Throws on malformed | Decodes & = ? # | Use case | Requires install |
|---|---|---|---|---|---|---|
| decodeURIComponent() | β all | β no | β URIError | β yes | Individual values and path segments | No |
| decodeURI() | β most | β no | β URIError | β no | Complete URL strings | No |
| URLSearchParams | β all | β yes | β silent | β yes | Query string parsing with automatic decode | No |
| URL constructor | β all | β yes | β TypeError | β yes | Full URL parsing and normalisation | No |
| decode-uri-component | β all | β no | β silent | β yes | Batch decode with malformed-input tolerance | npm install |
| querystring.unescape() | β all | β no | β silent | β yes | Legacy 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
Related Tools
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.
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.
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.