TL;DR
You passed a malformed or relative URL to new URL(), fetch(), or another API that demands a fully valid absolute URL. Two-second fix: make sure the URL has a scheme (https:// or http://). For relative paths, pass the base URL as the second argument.
// Broken
const url = new URL('/api/users'); // TypeError [ERR_INVALID_URL]: Invalid URL
// Fixed
const url = new URL('/api/users', 'https://example.com');
console.log(url.href); // https://example.com/api/users
Root Cause
Node.js follows the WHATWG URL standard โ the same spec browsers use. That means new URL(input, base?) is strict by design: pass a relative path with no base, and it throws immediately, no questions asked.
These are the situations that reliably trigger the error:
- Relative path with no base:
new URL('/path') - URL is
undefined,null, or an empty string - URL contains spaces or unencoded special characters like
#or| - Protocol is missing:
new URL('example.com/path')โ looks valid, isn't - Variable interpolation gone wrong โ the string ends up being something unexpected
- Calling
fetch('/api/data')in Node.js โ unlike browsers, Node has no implicit base URL context
Fix Approaches
1. Relative URL โ pass the base as the second argument
This is the most common mistake. Browsers have window.location as an implicit base; Node.js doesn't. You have to supply it explicitly.
// Wrong
const u = new URL('/users/123');
// Right
const u = new URL('/users/123', 'https://api.example.com');
console.log(u.href); // https://api.example.com/users/123
2. URL is undefined or null
Classic env var trap โ process.env.API_URL is undefined when the variable isn't set, and new URL(undefined) blows up immediately.
const endpoint = process.env.API_URL; // undefined if not set
new URL(endpoint); // TypeError [ERR_INVALID_URL]: Invalid URL
// Fix: validate before using
if (!endpoint) throw new Error('API_URL env var is not set');
const url = new URL(endpoint);
3. Missing protocol / bare hostname
api.example.com/users looks like a valid URL. It isn't โ at least not to the WHATWG parser, which requires an explicit scheme.
// Wrong โ no scheme
new URL('api.example.com/users');
// Right
new URL('https://api.example.com/users');
4. Spaces or invalid characters in the URL
Any URL built from user input or dynamic data needs encoding first. URLSearchParams handles this automatically โ use it instead of template literals.
const searchTerm = 'hello world';
// Wrong
const u = new URL(`https://example.com/search?q=${searchTerm}`);
// Right โ URLSearchParams encodes for you
const u = new URL('https://example.com/search');
u.searchParams.set('q', searchTerm);
console.log(u.href); // https://example.com/search?q=hello+world
5. fetch() with a relative URL in Node.js
Node 18+ ships native fetch(), but there's a catch: unlike the browser, it has no base URL. Relative paths that work fine in React or the browser will fail here. Always pass absolute URLs.
// Wrong in Node.js (works in browser, breaks here)
const res = await fetch('/api/data');
// Right
const BASE = process.env.API_URL ?? 'http://localhost:3000';
const res = await fetch(`${BASE}/api/data`);
6. Wrap with try/catch for user-supplied input
Don't trust external input. Wrap it, catch the error, and fail gracefully instead of crashing the process.
function parseUrl(input) {
try {
return new URL(input);
} catch {
return null; // or throw a friendlier error
}
}
const result = parseUrl(userInput);
if (!result) {
console.error('Invalid URL provided:', userInput);
}
7. Building URLs dynamically โ use the URL API properly
String concatenation is fragile โ one stray slash or unencoded character and you're back to this error. Let the URL API handle path and query construction instead.
const base = new URL('https://api.example.com');
const endpoint = new URL('/v2/users', base);
endpoint.searchParams.set('page', '2');
endpoint.searchParams.set('limit', '50');
console.log(endpoint.href);
// https://api.example.com/v2/users?page=2&limit=50
Verification
Drop this into a file called check-url.js and run it with node check-url.js. Every line should print OK:. A FAIL: line tells you exactly which case is still broken.
const cases = [
['https://example.com/path', undefined],
['/relative', 'https://example.com'],
['https://example.com/search', undefined],
];
for (const [input, base] of cases) {
try {
const u = base ? new URL(input, base) : new URL(input);
console.log('OK:', u.href);
} catch (e) {
console.error('FAIL:', input, '->', e.message);
}
}
Quick Checklist
- Does the URL start with
http://orhttps://? - Is the variable actually a string โ not
undefinedornull? - For relative paths: is a base URL passed as the second argument?
- Any unencoded spaces or special characters in the URL?
- If using
fetch()in Node.js: is the URL absolute?
Further Reading
- Node.js docs: WHATWG URL API โ new URL(input, base)
- MDN: URL() constructor
- WHATWG URL Standard: url.spec.whatwg.org

