10LOC
Web Platformintermediate

Use URL.canParse for safer URL handling

Published September 3, 2027

export const parseValidUrls = (candidates: string[], base?: string): URL[] =>
  candidates.filter((candidate) => URL.canParse(candidate, base)).map((candidate) => new URL(candidate, base));

export const isSameOriginRedirect = (target: string, currentOrigin: string): boolean => {
  if (!URL.canParse(target, currentOrigin)) return false;
  return new URL(target, currentOrigin).origin === currentOrigin;
};

What

URL.canParse(url, base?) is a static method that returns true or false for whether url (optionally resolved against base) is a parsable URL — the exact same parsing new URL() does, minus the object allocation and minus the thrown exception on failure.

Why it matters

Before this existed, checking "is this string a valid URL" meant wrapping new URL(candidate) in a try/catch and discarding the result — allocating a URL object you don't want, just to use its constructor as a validity check, and reaching for exception handling to do what's fundamentally a boolean question. That pattern is also easy to get subtly wrong: forgetting the base argument on a relative URL throws for a different reason than an actually-malformed URL, and try/catch swallows both identically unless you inspect the error. canParse separates the question ("would this parse?") from the action ("give me the parsed result"), so validation and construction are two explicit steps instead of one overloaded try/catch.

How it works

  • parseValidUrls filters a list of candidate strings down to the ones URL.canParse accepts, then constructs URL objects only for the ones known to succeed — no exceptions thrown in the happy path, no wasted allocations for the rejected ones.
  • isSameOriginRedirect guards new URL() behind a canParse check first, then compares .origin against currentOrigin — a realistic shape for validating a ?redirect= query parameter before honoring it, which is exactly the kind of input an open-redirect vulnerability comes from.
  • Both functions pass base straight through to canParse and to new URL() identically — canParse returning true is a guarantee that the same call to new URL() will not throw, not just a loose approximation.

Gotchas

  • canParse answers "is this syntactically a URL," not "is this URL safe to fetch, redirect to, or trust." A same-origin check like isSameOriginRedirect is still necessary on top of it — a perfectly parsable URL can still point somewhere you don't want to send a user.
  • Passing base changes what counts as valid: URL.canParse("/path") is false (no base to resolve a relative path against), but URL.canParse("/path", "https://example.com") is true. If you're validating strings that might be either absolute or relative, decide up front whether you're supplying a base.
  • Support arrived within the same few months across engines but at different exact versions: Chrome 120 (December 2023), Firefox 115 (July 2023), Safari 17 (September 2023) — safe to use unguarded in any project targeting current browsers, but check your minimum supported versions if you maintain an older baseline.
  • It's available in Web Workers too, unlike some URL-adjacent APIs — no main-thread-only restriction here.

Related

  • new URL() in a try/catch — the pattern this replaces; still necessary in environments without canParse, or when you want the parsed URL and the validity check in one step.
  • URLPattern — for matching a URL against a route-like pattern rather than just checking parsability; a different question with a different API.