Failed to Construct 'URL': Invalid URL — Every Real Cause and How to Fix It

Published 15 Sep 2026 · 8 min read

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.

Quick answer: 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.

SymptomMost likely causeFix
Input looks like a domain but has no http:// or https://Missing schemePrepend the protocol before constructing the URL
Input starts with // and still failsProtocol-relative string, no scheme of its ownPrepend a scheme, or pass a base so the current page's scheme is inherited
Input is a bare path like /api/usersRelative path, but new URL() has no implicit basePass a second argument: new URL('/api/users', 'https://api.example.com')
Fails intermittently, often after a config or env changeEmpty string or an unset variable stringified to "undefined"Validate the value exists before constructing the URL
Fails only for one specific pasted or generated URLA stray character (like <, |, or ^) inside the hostFix 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:

Open this example in URL Tool

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:

Open this example in URL Tool

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:

Open this example in URL Tool

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:

Open this example in URL Tool

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:

Open this example in URL Tool

What does not cause this error

Three things that look like they should break parsing, verified directly rather than assumed, don't:

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 Tool

Browser 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:

EnvironmentError text
Browsers (Chrome, Firefox, Safari)TypeError: Failed to construct 'URL': Invalid URL
Node.jsTypeError [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

  1. console.log the exact string right before it reaches new URL() — don't debug from what the variable was supposed to contain.
  2. Check for a scheme first: does the string start with http://, https://, or another real scheme? If not, that's causes 1 or 2.
  3. If it's a bare path starting with / (and not //), you're missing the second base argument — cause 3.
  4. If the logged value is literally the four characters undefined, or empty, trace back to whatever produced it — cause 4.
  5. 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.
  6. 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 →