Every codebase has this utility function somewhere. You wrote it the first time an untrusted URL caused an uncaught exception, or when a form field needed validation before submission, or when an API returned a path instead of a full URL. It's the try/catch wrapper, and it works. The problem is that every team writes their own version, they differ in subtle ways, and they shouldn't need to exist. URL.canParse() and URL.parse() are the native replacements — shipping now in every modern browser.
The wrapper you've been maintaining
The new URL() constructor throws a TypeError on invalid input. That's correct when your input is a string literal you own — an exception is the right signal that your own code has a bug. It's the wrong behavior when you're parsing user input, a third-party API response, or a redirect URL from an HTTP header.
The result is this pattern:
function safeParseURL(str, base) {
try {
return new URL(str, base);
} catch {
return null;
}
}
A bad URL in a form field is not a programmer error. Exceptions used for expected control flow make call sites harder to read and suppress details that would help debugging. This wrapper should not need to exist.
URL.canParse() — validate without constructing
When you only need to know whether a string is a valid URL — form validation, link display logic, redirect safety checks — URL.canParse() gives you a boolean with no allocation:
URL.canParse('https://example.com') // true
URL.canParse('not a url') // false
URL.canParse('javascript:alert(1)') // true — valid URL, unsafe redirect
URL.canParse('/about', 'https://example.com') // true
URL.canParse('::bad', 'https://example.com') // false
The second argument is a base URL, resolved the same way new URL() resolves it. This matters when you're validating relative paths — from a <base> tag, from API responses that return paths rather than full URLs, or from a <a href> value you want to check before following.
One important note: URL.canParse() validates the URL's syntax. It tells you whether the string can be parsed as a URL. It does not tell you whether the URL is safe to navigate to — javascript: and data: URLs are syntactically valid.
URL.parse() — the non-throwing constructor
URL.parse() does what new URL() does but returns null instead of throwing when the input is invalid:
const url = URL.parse(userInput);
if (!url) {
showError('Not a valid URL.');
return;
}
// url is a URL instance — all properties and methods are available
console.log(url.hostname);
console.log(url.pathname);
url.searchParams.set('ref', 'footer');
Every property and method available on a URL object — hostname, pathname, searchParams, href — works the same way. The only difference from new URL() is what happens on invalid input: null instead of a thrown exception.
Like URL.canParse(), the second argument is a base URL:
const url = URL.parse('/products/42', 'https://example.com');
// → URL { href: 'https://example.com/products/42', pathname: '/products/42', ... }
const relative = URL.parse('../images/photo.jpg', 'https://cdn.example.com/assets/v2/');
// → URL { href: 'https://cdn.example.com/assets/images/photo.jpg', ... }
🎮 Try it yourself
▶️ Open the interactive playground →
Runs right in your browser — poke at it and watch the concept react live.
Which to use and when
There are now three ways to get a URL object, and each has a clear use case:
-
new URL(str)— when the input is yours and invalid input is a bug. Use this for URL construction from values you control. The thrown exception surfaces real mistakes immediately. -
URL.canParse(str)— when you only need validity, not the URL object. Form validation, display logic, redirect checking. Zero allocation — it doesn't construct anything. -
URL.parse(str)— when you need the URL object and the input might be invalid. This replaces everysafeParseURL()wrapper in your codebase.
The common antipattern to avoid: calling URL.canParse() immediately before URL.parse() or new URL(). That parses the string twice.
// Antipattern: double-parse
if (URL.canParse(input)) {
const url = new URL(input); // parsing the same string again
use(url);
}
// Correct: parse once, check for null
const url = URL.parse(input);
if (url) {
use(url);
}
Reach for URL.canParse() when null is the only thing you need to check — for example, gating a conditional that doesn't need the URL properties at all.
TypeScript support
TypeScript added type signatures for both methods in version 5.1 (May 2023). No extra @types package is needed — if your tsconfig.json includes "DOM" in the lib array, they're typed automatically:
function getCanonicalHost(input: string): string | null {
const url = URL.parse(input);
return url ? url.hostname : null;
}
TypeScript knows URL.parse() returns URL | null, so the null check narrows the type to URL before you access its properties. No cast needed.
Browser support
-
URL.canParse(): Baseline Widely Available — Chrome 120 (January 2024), Firefox 115 (July 2023), Safari 17 (September 2023). Also available in Node.js 19.9 and Deno 1.33. -
URL.parse(): Baseline 2024 — Chrome 126 (June 2024), Firefox 126 (June 2024), Safari 18 (September 2024). Node.js 22.1.
Both are safe for production in modern browser targets. For legacy support, the safeParseURL pattern above is still the correct polyfill — but with a clear feature-detect, you can retire it progressively.
🧠 Test yourself
Think it clicked? Take the 7-question quiz →
Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.
The takeaway
Search your codebase for functions named safeParseURL, tryParseUrl, parseUrlSafely, or any utility with new URL() inside a try/catch block. Each one can be replaced with a direct call to URL.parse(): same return type, same base-URL support, no wrapper to maintain, and no subtle behavioral difference between implementations written by different developers at different times.
Keep new URL() only where the input is guaranteed to be valid — URL construction from your own literals, config values, or template strings. Let the exception do its job there, and let URL.parse() handle the cases where invalid input is expected.
Thanks for reading! Let's stay connected:
- ⭐ GitHub — follow me and star the projects: github.com/parsajiravand
- 💬 Discord — join the frontend best-practices community: discord.gg/d9KRhuAwQ
- 📸 Instagram — frontend best practices, daily: @bestpractice___
- 💼 LinkedIn — linkedin.com/in/parsa-jiravand
- ✉️ Email (work & contract inquiries): bestpractice2026@gmail.com