Quick answer
The string passed to new URL(...) isn't a complete, valid absolute URL — usually a relative path with no base, a missing protocol, or genuinely malformed input. Fix it with the two-argument form new URL(path, base) for relative paths, or wrap construction in try/catch when the string comes from user input.
The exact error string
new URL("/api/users");
// Uncaught TypeError: Failed to construct 'URL': Invalid URL
new URL("example.com");
// Uncaught TypeError: Failed to construct 'URL': Invalid URL
// -> no protocol, so this isn't a recognizable absolute URL
Why this throws instead of returning something empty
Unlike most JavaScript string handling, the URL constructor validates its input against the URL spec immediately and synchronously — there's no "empty URL" fallback the way Number("abc") gives you NaN. If the input isn't a parseable absolute URL (and no base was given to resolve a relative one against), it throws right there, which is exactly why this needs a try/catch around any URL coming from outside your control.
Cause 1: a relative path with no base
new URL("/api/users");
// ❌ relative path — no scheme, no host
new URL("/api/users", "https://example.com"); // ✅ two-argument form
// -> https://example.com/api/users
new URL("/api/users", window.location.origin); // ✅ resolve against the current page
Cause 2: a missing protocol
new URL("example.com");
// ❌ looks like a bare hostname, not a full URL
new URL("https://example.com"); // ✅ explicit protocol
// or detect and normalize user-typed input:
function toAbsoluteUrl(input) {
return /^https?:\/\//i.test(input) ? input : "https://" + input;
}
new URL(toAbsoluteUrl("example.com")); // ✅
Cause 3: unvalidated user input
function isValidUrl(value) {
try {
new URL(value);
return true;
} catch {
return false; // ✅ bad input is expected, not exceptional — handle it, don't crash
}
}
if (isValidUrl(userInput)) {
const url = new URL(userInput);
// ... use it
} else {
showError("Please enter a valid URL, including https://");
}
Cause 4: a broken template literal
const base = "https://api.example.com"; // no trailing slash
const path = "users";
new URL(`${base}/${path}`); // ✅ https://api.example.com/users
const endpoint = undefined;
new URL(`${base}/${endpoint}`);
// ❌ builds "https://api.example.com/undefined" — technically parses,
// but is not the URL you meant; log the built string to catch this
Common variants at a glance
| Input | Why it fails | Fix |
|---|---|---|
/path | relative, no base | new URL(path, base) |
example.com | no protocol | prepend https:// |
| arbitrary user text | not a URL at all | try/catch or validate first |
${base}${path} | missing separator / undefined variable | log the built string before constructing |
| empty string | nothing to parse | check for empty before constructing |
Debugging checklist
- ✓ Log the exact string right before
new URL(...)runs - ✓ Relative path? Add the base as the second argument
- ✓ No
http(s)://? Add it before constructing - ✓ Value comes from a user or external source? Wrap in
try/catch - ✓ Built with a template literal? Confirm no piece is
undefinedand separators are correct
Frequently Asked Questions
What does 'Failed to construct URL: Invalid URL' mean?
The string you passed to new URL(...) is not a valid absolute URL, and no second base-URL argument was given to resolve it against. Unlike most string operations, the URL constructor validates its input immediately and throws synchronously if it can't parse a complete URL from what it was given.
Why does a relative path throw this?
new URL('/api/users') fails because a relative path has no scheme or host — the constructor needs a full, absolute URL unless you also provide a base as the second argument: new URL('/api/users', 'https://example.com'). The two-argument form is exactly how the browser resolves relative links against a page's own URL.
Why does a missing protocol cause this?
new URL('example.com') throws because without http:// or https://, the string looks like a relative path (or an opaque scheme-less value), not an absolute URL. Prepend the protocol explicitly, or detect and add it before constructing: value.startsWith('http') ? value : 'https://' + value.
Why does user input break this?
A text field where users type a URL will inevitably get malformed input — spaces, missing protocol, extra characters, or a value that isn't a URL at all. Never call new URL(userInput) unguarded; wrap it in try/catch or validate the string first, since a bad value is expected user behavior, not an exceptional case.
Why does a template literal produce an invalid URL?
A common bug is an unresolved template variable or a missing separator: new URL(`${base}${path}`) can produce something like 'https://api.example.compath' if base doesn't end in a slash, or embed "undefined" if a variable wasn't set. Log the built string before passing it to new URL to catch this.
How do I validate a URL without throwing?
Wrap the construction in try/catch and treat a thrown error as "not a valid URL" rather than letting it crash the page: function isValidUrl(s) { try { new URL(s); return true; } catch { return false; } }. This is the standard, spec-correct way to validate a URL string in JavaScript.
Building or decoding URLs?
Encode/decode query strings and URL components entirely in your browser — nothing is uploaded to a server.