Failed to Construct 'URL': Invalid URL — Every Real Cause and How to Fix It
TypeError: Failed to construct 'URL': Invalid URL is what a browser throws whenever new URL(input) can't make sense of input. Node.js throws the same underlying failure with different wording — TypeError [ERR_INVALID_URL]: Invalid URL — and neither message says which of the string's characters broke parsing, because the WHATWG URL parser is pass/fail, not diagnostic. This guide covers every real cause, verified directly against Node's own URL implementation rather than assumed, with a live, prefilled URL Tool example for each so you can watch the exact input fail instead of reading a description of one.
new URL(input) with a single argument requires input to already be a complete, absolute URL — scheme included. There is no implicit base to resolve a relative string against, unlike fetch() or an anchor's href. If your string is missing a scheme, is protocol-relative (//host/path), is a bare path (/api/users), or is empty/undefined, parsing fails before any of the rest of the string is even considered. Pass a second base argument — new URL(input, base) — when input is genuinely meant to be relative.
Quick reference
All five cases below were verified against Node.js's native URL constructor (the same WHATWG implementation browsers ship) before publishing, then reproduced live in URL Tool's own parser to confirm the identical input fails there too.
| Symptom | Most likely cause | Fix |
|---|---|---|
Input looks like a domain but has no http:// or https:// | Missing scheme | Prepend the protocol before constructing the URL |
Input starts with // and still fails | Protocol-relative string, no scheme of its own | Prepend a scheme, or pass a base so the current page's scheme is inherited |
Input is a bare path like /api/users | Relative path, but new URL() has no implicit base | Pass a second argument: new URL('/api/users', 'https://api.example.com') |
| Fails intermittently, often after a config or env change | Empty string or an unset variable stringified to "undefined" | Validate the value exists before constructing the URL |
| Fails only for one specific pasted or generated URL | A stray character (like <, |, or ^) inside the host | Fix the string that built the host; don't rely on the parser to guess |
1. Missing scheme (protocol)
The single most common cause, and the one every search result agrees on: a string that reads like a URL to a human — a domain, maybe a path — but never actually states its scheme. new URL() has no default to fall back on; without http:// or https:// (or any other registered scheme), the parser has no way to know this is a URL at all rather than, say, a relative path that happens to contain dots.
new URL('api.example.com/v1/users');
// TypeError: Failed to construct 'URL': Invalid URL
// Fix: state the scheme explicitly
new URL('https://api.example.com/v1/users'); // OK
This shows up constantly when a URL is assembled from a config value that someone entered without the protocol (a "just put the domain" field in an admin panel), or when a value copied from a browser's address bar loses its scheme in transit — some UIs hide https:// visually and a copy-paste only grabs what's shown.
See it fail live
This is exactly what a domain-only config value looks like once it reaches new URL() with nothing prepended:
2. Protocol-relative strings (//host/path)
A protocol-relative URL — starting with // and no scheme of its own — is valid in an HTML attribute (<script src="//cdn.example.com/app.js">) or a CSS url(), where the browser inherits the current page's scheme automatically. It is not valid on its own to the URL constructor: from the parser's point of view it's still missing a scheme, the same failure as cause 1, just dressed up to look absolute because it starts with a double slash.
new URL('//cdn.example.com/analytics.js');
// TypeError: Failed to construct 'URL': Invalid URL
// Fix: give it a scheme, either literally or via a base
new URL('https://cdn.example.com/analytics.js'); // OK
new URL('//cdn.example.com/analytics.js', location.href); // OK — inherits location's scheme
This one trips people up specifically because the string looks complete — it has a host, a path, even the double slash a URL is supposed to start with. It's copied straight out of markup where it worked fine, and only breaks once it's handed to new URL() directly with no base.
See it fail live
A CDN reference lifted straight from a <script src> attribute, where it was perfectly valid:
3. A relative path, expecting the implicit base fetch() gets
This is the mistake underneath a lot of "it works in one place and not another" bug reports. fetch('/api/users'), <a href="/api/users">, and xhr.open('GET', '/api/users') all silently resolve that path against the current page's URL — the browser supplies an implicit base for you. new URL('/api/users') gets no such treatment. Called with one argument, the WHATWG spec hands the parser undefined as the base, so a relative path has nothing to resolve against and fails outright.
fetch('/api/users'); // OK — browser resolves against location.href
new URL('/api/users');
// TypeError: Failed to construct 'URL': Invalid URL
// Fix: supply the base explicitly
new URL('/api/users', 'https://api.example.com'); // OK
new URL('/api/users', location.href); // OK, in a browser
Server-side code hits this even more often than browser code, because there's no location to reach for by habit — the base has to be a real, deliberate value (a configured API origin, a request's own Host header) rather than something implicit.
See it fail live
A route path passed to new URL() exactly as it would be passed to fetch() — except fetch() wouldn't have minded:
4. Empty string, or a variable that was never set
new URL() coerces whatever it's given to a string before parsing it. Pass undefined — an environment variable that was never set, a config key that doesn't exist, a function argument nobody passed — and it doesn't fail with a message about a missing value. It fails because String(undefined) is the four-character string "undefined", and "undefined" is not a valid URL any more than any other word is.
const base = process.env.API_BASE_URL; // typo'd env var name; actually undefined
new URL(base);
// TypeError: Failed to construct 'URL': Invalid URL
// (not "API_BASE_URL is not set" — the message gives no hint the value was ever undefined)
The fix is to validate the value exists before it ever reaches new URL(), specifically so the failure you get points at the real problem instead of a generic parse error:
if (!base) throw new Error('API_BASE_URL is not set');
new URL(base);
See it fail live
The literal string "undefined" — exactly what an unset variable becomes once it's coerced to a string:
5. A stray character inside the host
Not every broken character breaks parsing — the parser is deliberately forgiving about the path, percent-encoding spaces and other reserved characters there automatically. The host is stricter: characters like <, |, and ^ aren't valid there and the parser refuses to guess what you meant, because it has no repair rule for a broken domain name the way it does for a path.
new URL('https://api<example.com/v2/orders'); // stray "<" landed in the host
// TypeError: Failed to construct 'URL': Invalid URL
new URL('https://api.example.com/v2/orders<script>'); // same character in the PATH — this one is fine
// -> https://api.example.com/v2/orders%3Cscript%3E
This usually comes from string concatenation gone wrong — a template literal missing a variable, two separately-built strings glued together without a separator, or user input dropped straight into the hostname position without validation. Because the path tolerates the same character just fine, "it fails for URL A but not URL B" is a strong hint the bad character landed in the host specifically. An unterminated IPv6 literal (https://[::1, missing the closing ]) and a non-numeric or out-of-range port (:abc, :99999) fail the same way, for the same reason: something inside the authority portion of the URL doesn't parse.
See it fail live
A stray < landed inside the host during string concatenation — everything else about this URL is well-formed:
What does not cause this error
Three things that look like they should break parsing, verified directly rather than assumed, don't:
- Leading or trailing whitespace.
new URL(' https://example.com ')parses fine — surrounding whitespace is stripped before parsing begins. - A tab, newline, or carriage return anywhere in the string. These are stripped outright wherever they appear, not just at the ends — spec-defined control-character removal, not a bug in your input.
- A literal space inside the host — sometimes. This is the one genuinely surprising, runtime-dependent result here, verified directly rather than assumed: Chrome's
new URL()percent-encodes a bare space in the host to%20instead of rejecting it, while Node.js throwsInvalid URLfor the exact same string. Don't assume a space is (or isn't) the cause without checking which runtime actually threw the error you're looking at —<,|, and^from cause 5, by contrast, fail consistently everywhere.
If a URL is failing and one of the whitespace cases above is the only thing that looks unusual about it, keep looking — the real cause is one of the five above.
See it not fail, live
Trailing whitespace after a valid URL. URL Tool's Architect View still parses it cleanly, because this was never actually broken:
Open this example in URL ToolBrowser vs. Node.js: same failure, different wording
Both runtimes implement the identical WHATWG URL Standard "basic URL parser" — the actual parsing logic that decides an input is invalid is shared, not reimplemented differently per platform. What differs is only how each runtime reports that failure:
| Environment | Error text |
|---|---|
| Browsers (Chrome, Firefox, Safari) | TypeError: Failed to construct 'URL': Invalid URL |
| Node.js | TypeError [ERR_INVALID_URL]: Invalid URL |
If you've searched one phrasing and only found results for the other, that's why — they're the same bug, reported by two different runtimes wrapping the same spec-defined parser. All five causes above throw in both environments identically; only the wrapper text changes.
Debugging checklist
console.logthe exact string right before it reachesnew URL()— don't debug from what the variable was supposed to contain.- Check for a scheme first: does the string start with
http://,https://, or another real scheme? If not, that's causes 1 or 2. - If it's a bare path starting with
/(and not//), you're missing the secondbaseargument — cause 3. - If the logged value is literally the four characters
undefined, or empty, trace back to whatever produced it — cause 4. - If the string has a scheme, a host, and still fails, look character-by-character at the host portion specifically (not the path) for anything that isn't a letter, digit, dot, or hyphen — cause 5.
- If none of the above fit and the string merely has odd whitespace, that's not actually the cause — keep looking.
This is a parsing failure, not an encoding one — it happens before any percent-encoding is involved. If your URL parses fine but a query parameter or path segment looks wrong once you read it back, that's a different problem: see our URL encoding and decoding guide for percent-encoding and query-parameter handling instead.
🔗 Paste your URL and see exactly where it breaks
URL Tool parses, encodes, decodes, and inspects URLs entirely in your browser — nothing you paste is ever uploaded anywhere.
Open URL Tool →← Back to Blog · See also: URL Encoding and Decoding Guide →