TypeError: Failed to construct 'URL': Invalid URL

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

What it actually looks like

Chrome DevTools console showing a red error row reading Uncaught TypeError: Failed to construct 'URL': Invalid URL, sourced to app.js line 3.
Captured from Chrome 152. The wording is browser-specific and worth knowing: Node raises the identical failure as TypeError: Invalid URL with code: 'ERR_INVALID_URL' and no Failed to construct prefix. Same bug, two search strings.

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

The same failure, two different messages

The wording in the heading of this page is browser-specific. The Failed to construct 'URL': prefix comes from the WebIDL layer that wraps constructors in the browser, and Node has no such layer — so identical code produces a visibly different string depending on where it runs:

// Chrome 152 (and any Chromium browser)
Uncaught TypeError: Failed to construct 'URL': Invalid URL

// Node.js v24.15.0 — same bug, different text, plus a machine-readable code
TypeError: Invalid URL
    at new URL (node:internal/url:825:25) {
  code: 'ERR_INVALID_URL',
  input: '/api/users'
}

Two practical consequences. First, if you searched one form and found nothing useful, search the other. Second, Node's version is strictly more helpful for debugging: it attaches code: 'ERR_INVALID_URL' (stable across versions, so safe to branch on) and an input property containing the exact string that failed — which is the one thing the browser message never tells you.

try {
  new URL(value);
} catch (err) {
  if (err.code === "ERR_INVALID_URL") {
    console.error("bad URL:", err.input);   // ✅ Node prints the offending string
  }
  throw err;
}

Check without throwing: URL.canParse() and URL.parse()

The try/catch in Cause 3 above is no longer the only option. Two static methods now answer the same question without using exceptions for control flow:

URL.canParse("/api/users");                          // false — returns a boolean, never throws
URL.canParse("/api/users", "https://api.example.com"); // true  — base makes it resolvable

URL.parse("/api/users");                             // null instead of throwing
const url = URL.parse(userInput, base);
if (url === null) {
  showError("Please enter a valid URL, including https://");
} else {
  fetch(url);                                        // ✅ already a URL object
}

Both take the same (input, base) arguments as the constructor. URL.canParse() landed in Chrome 120, Firefox 115, Safari 17 and Node 19.9; the static URL.parse() is newer — Chrome 126, Firefox 126, Safari 18 and Node 22.1. Prefer URL.parse() where your baseline allows it: canParse() followed by new URL() parses the string twice, while parse() validates and gives you the object in one pass. If you must support older targets, keep the try/catch.

The base argument: a trailing slash changes the answer

Once you start passing a base URL to fix Cause 1, a subtler bug replaces the original one. The base is resolved using the normal relative-URL rules, which means the last path segment of the base is discarded unless the base ends in a slash:

new URL("users", "https://api.example.com/v1/").href
// ✅ "https://api.example.com/v1/users"   — base ends in "/", v1 is a directory

new URL("users", "https://api.example.com/v1").href
// ❌ "https://api.example.com/users"      — no slash, so "v1" is replaced

new URL("/users", "https://api.example.com/v1/").href
// ❌ "https://api.example.com/users"      — a leading "/" ignores the base path entirely

All three of these parse successfully, so nothing throws — you get a 404 from your API instead, which is a much longer debugging session than an exception would have been. Verified identically on Chrome 152 and Node v24.15.0. The rule worth memorising: base paths end in a slash, relative paths do not begin with one. When a request 404s just after you added a base URL, log url.href before the fetch and this will be visible immediately.

Common variants at a glance

InputWhy it failsFix
/pathrelative, no basenew URL(path, base)
example.comno protocolprepend https://
arbitrary user textnot a URL at alltry/catch or validate first
${base}${path}missing separator / undefined variablelog the built string before constructing
empty stringnothing to parsecheck for empty before constructing

Debugging checklist

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.

References

Building or decoding URLs?

Encode/decode query strings and URL components entirely in your browser — nothing is uploaded to a server.

URL Encoder & Decoder All Error References HTTP Status Codes
About the author

Pasindu Ishan is a Senior Software Engineer working on data streaming systems, which means most days end at a JSON payload that will not parse. The JavaScript references here are checked against a real runtime before being written up. He builds the free in-browser tools at JSON Dev Tools.