In browser JavaScript, what is the second argument to the URL constructor for, and why does new URL('/checkout') throw while new URL('/checkout', 'https://shop.example.com/cart/items') does not?
answer
- two arguments, not one
- the second one supplies the origin
- relative input has nothing to resolve against
- throws, never returns null
- trailing slash on the base changes the answer
basics
~20 sThe second argument is a base URL that a relative reference is resolved against. A relative string on its own has no scheme or host, so new URL('/checkout') throws a TypeError; with the base it resolves to https://shop.example.com/checkout.
solid answer
~40 s`new URL(input, base)` runs the WHATWG URL parser: if `input` is already absolute the base is ignored, and if it is relative the base supplies the missing scheme, host and path context. `'/checkout'` is a path-absolute reference — it has no origin of its own — so calling `new URL('/checkout')` with no base throws a `TypeError`, while `new URL('/checkout', 'https://shop.example.com/cart/items')` gives `https://shop.example.com/checkout`. The resolution rules matter: a leading `/` replaces the whole path, a bare relative like `'items'` replaces only the last path segment of the base, and `'?x=1'` or `'#top'` keep the base path. In page code the base is usually `location.href` or `document.baseURI`. Because the constructor throws on anything unparseable, wrap untrusted input in `try`/`catch`, or use `URL.canParse()` where it is available.
code
javascript · 13 linesconst base = 'https://shop.example.com/cart/items';
console.log(new URL('https://other.test/x', base).href);
console.log(new URL('/checkout', base).href);
console.log(new URL('checkout', base).href);
console.log(new URL('checkout', base + '/').href);
console.log(new URL('#top', base).href);
try {
new URL('/checkout');
} catch (err) {
console.log(err.name);
}go deeper
Know that the constructor takes an optional base URL, that relative input without a base throws a TypeError, and that location.href is the base you normally pass in page code.
Be ready to walk the resolution table out loud: absolute input ignores the base, a leading slash replaces the path, a bare segment replaces the base's last segment, and a query- or hash-only reference keeps the rest.
Show that you treat untrusted URL input as a parse that can fail — try/catch or URL.canParse — and that you know parsing normalises case, default ports and dot segments, which matters when URLs become cache or dedupe keys.
Frame it as a codebase policy: one URL-building helper on top of the platform parser, base URLs stored with a trailing slash, no ad-hoc concatenation anywhere, so encoding and resolution bugs cannot be reinvented per feature.
## What the URL constructor is `URL` is a platform class that parses a URL string into a structured, mutable object. It implements the WHATWG URL parser — the same algorithm the browser uses for links and for the `fetch` request URL — so it is the ground truth for what a string means as a URL, far more reliable than a regex or `split('?')`. The constructor has two parameters: `new URL(input, base)`. `input` is the URL or relative reference you want to parse; `base` is optional and is itself parsed as an absolute URL first. ## How the base argument resolves a relative reference If `input` parses as an absolute URL on its own — it has a scheme like `https:` — the base is ignored entirely. Otherwise `input` is a *relative reference* and the parser fills in the missing pieces from the base: ```js const base = 'https://shop.example.com/cart/items?a=1#frag'; new URL('https://other.test/x', base).href; // 'https://other.test/x' (base ignored) new URL('/checkout', base).href; // 'https://shop.example.com/checkout' new URL('checkout', base).href; // 'https://shop.example.com/cart/checkout' new URL('../checkout', base).href; // 'https://shop.example.com/checkout' new URL('?b=2', base).href; // 'https://shop.example.com/cart/items?b=2' new URL('#top', base).href; // 'https://shop.example.com/cart/items?a=1#top' new URL('//cdn.test/x', base).href; // 'https://cdn.test/x' (scheme kept) ``` The two that trip people up are the third and fourth lines. A **bare** relative reference is resolved against the base's *directory*: the last segment of the base path is dropped and replaced. So the base `/cart/items` behaves as the directory `/cart/`, and `'checkout'` lands at `/cart/checkout`. Change the base to `/cart/items/` (trailing slash) and the same input lands at `/cart/items/checkout`. A missing or extra trailing slash on an API base is one of the most common URL-building bugs. ## Why the no-base call throws `'/checkout'` carries no scheme and no host. There is nothing to resolve it against, so the parser fails, and a failed parse in the constructor is a `TypeError` — not `null`, not an empty string: ```js try { new URL('/checkout'); } catch (e) { console.log(e.name); } // 'TypeError' ``` This is deliberate: the constructor never returns a half-parsed object. Any URL instance you hold is guaranteed to be a valid, absolute URL. In page code you almost always have a base available — `location.href` for "relative to the current page" or `document.baseURI` if the document has a `<base>` element — so `new URL(path, location.href)` is the idiomatic way to turn a path into a full URL. ## Reading the parsed parts Once parsed, the object exposes the components as properties. Note which ones include their punctuation: ```js const u = new URL('https://[email protected]:8443/a/b/../c?q=1#top'); u.protocol; // 'https:' (with colon) u.origin; // 'https://shop.example.com:8443' (read-only) u.host; // 'shop.example.com:8443' (with port) u.hostname; // 'shop.example.com' (no port) u.port; // '8443' (a string; '' when the scheme's default port is used) u.pathname; // '/a/c' (dot segments already resolved) u.search; // '?q=1' (leading '?', or '' when empty) u.hash; // '#top' (leading '#', or '' when empty) ``` Parsing also normalises: the scheme and host are lower-cased, a default port (`:443` on `https`) is dropped, and `.`/`..` segments are resolved. `origin` is read-only; every other component is writable, and assigning to one re-serialises `href`. ## Parsing without throwing When the input is untrusted — a value from a form field, a query parameter, a config file — you must handle failure. Three options: wrap the constructor in `try`/`catch`; call `URL.canParse(input, base)`, which returns a boolean and shipped across browsers in 2023; or call the static `URL.parse(input, base)`, which returns a `URL` or `null` instead of throwing and landed in 2024-era browser releases. Feature-detect the newer static methods before relying on them, and keep the `try`/`catch` fallback for older engines. ## Why interviewers ask The alternative to this API is string surgery, and string surgery gets encoding, duplicate `?`, and relative resolution wrong in ways that only show up on production data. Knowing that the constructor throws, that it needs a base for relative input, and that a trailing slash changes where a bare relative reference lands is the difference between a URL helper that works and one that quietly produces `https://api.test/v1checkout`.
- How would you build a request URL relative to the page the user is currently on?Pass `location.href` as the base: `new URL('/api/items', location.href)`. If the document has a `<base>` element you may want `document.baseURI` instead, since that is what relative links in the markup resolve against. Either way you get an absolute URL back, with the current scheme, host and port already filled in.
- What is the difference between resolving against 'https://api.test/v1' and 'https://api.test/v1/'?Only for *bare* relative input. With the base `/v1`, `new URL('items', base)` gives `/items` — the last segment `v1` is treated as a file name and replaced. With `/v1/`, it gives `/v1/items`. Path-absolute input like `'/items'` ignores the difference entirely. This is why API base URLs are conventionally written with a trailing slash.
- Is url.origin writable, and how do you point an existing URL object at a different host?`origin` is read-only — assigning to it silently does nothing in sloppy mode and throws in strict mode. Set the individual components instead: `url.protocol`, `url.hostname`, and `url.port` are all writable, and each assignment re-serialises `url.href`. Often it is simpler to construct a fresh `URL` from the new absolute string.
saying these in an interview costs you the question
- Claiming new URL of a bad string returns null
- Thinking the base is used even for absolute input
- Assuming 'items' resolves under the base's full path
- Using split('?') or a regex instead of the parser
- Believing url.port is a number, not a string