JavaScript URL API Table
| Piece | What it does | Field note |
|---|---|---|
new URL(str) | Parses into components | Throws SyntaxError on garbage - wrap in try/catch for user input |
u.searchParams.get(k) | Read one query param | Decodes automatically - never manually unescape again |
u.searchParams.set(k, v) | Write a param | Encodes automatically; set replaces, append duplicates |
u.searchParams.has(k) | Presence check | Distinguishes ?a= from absence |
u.pathname / u.host | Path and host separately | No string slicing between ? and # |
u.hash | Fragment including # | Empty string when absent - not null |
URLSearchParams(u.search) | Standalone param iterator | entries(), forEach, and delete() that URL lacks directly |
u.toString() | Serialize back | Round-trips: parse, edit, serialize - encoding stays consistent |
The URL API is the browser's built-in URL parser: new URL(str) validates and splits a URL into components, searchParams reads and writes the query string with automatic encoding, and toString() serializes it all back. The table below is the working eight - everything real code needs from the last legitimate use of regex URL parsing.
Bottom line: parse once, edit through searchParams, serialize back - encoding stays consistent because the browser does it. Hand-rolled parsing fails in two predictable places: splitting the query on & explodes when a value contains an encoded &-lookalike, and forgetting that hash changes never reload the page turns subtle bugs into invisible ones. The URL object exists in every browser and Node since 2017; indexOf('?') code is a bug in waiting.
The honest part: the two quirks worth memorizing are both small. URL throws a SyntaxError on unparseable input - user-supplied URLs need try/catch. And u.hash is an empty string (not null) when no fragment exists, which makes truthiness checks work by accident and strict comparisons fail by design.
How to use
- Parse with new URL(str) inside try/catch - user input is untrusted and garbage URLs throw.
- Read and write query values exclusively through searchParams: get, set, append, delete, has cover every case with automatic encoding.
- Serialize with toString() or href - the round trip parse-edit-serialize is the whole API's promise.
Frequently asked questions
Why should I use the URL API instead of splitting on & and =?
Because query values legitimately contain encoded separators: a search for 'a&b' arrives as ?q=a%26b, and naive splitting on & survives that only by luck of the encoding - unescape first and the value explodes into two parameters. searchParams decodes each value exactly once, handles missing = signs, keeps repeated keys (getAll), and re-encodes on write. Every hand-rolled parser eventually meets a value it mangles; the API cannot be fed a value it mangles.
What is the difference between searchParams.get and has?
has answers whether the parameter exists at all; get returns its value or null. They disagree at ?flag= (exists, empty-string value: has is true, get returns '') and ?flag (exists with no value at all: same result). The distinction matters for toggles where presence itself is the signal - ?dark in a URL meaning 'force dark' - and for debugging APIs that distinguish empty from missing.
How do I add or change a query parameter without reloading?
Parse the current URL, edit searchParams, then history.replaceState(null, '', u) - the address bar updates without navigation. The full pattern: const u = new URL(location.href); u.searchParams.set('page', '2'); history.replaceState(null, '', u). This is how filter UIs and pagination keep URLs shareable and the back button meaningful - the URL becomes state storage instead of a location.
Why does my URL constructor throw on inputs that look fine?
Relative strings like /page?id=1 have no base to resolve against - new URL('/page') throws because a URL needs a scheme. Fix with the two-argument form: new URL('/page', location.origin) resolves the relative path against a base. User-supplied values throw for real reasons too (missing scheme, spaces) - the throw is the API telling you the string is not a URL, which plain string checks were silently guessing at.