How to Detect Broken Links Using JavaScript

detect broken links

Detect broken links using JavaScript with working HEAD-request code console snippet, full scanner with retry logic, and a Node.js sitemap checker.

Technical SEO Broken Links JavaScript 2026

Detect broken links using JavaScript

Detect broken links using JavaScript comes down to one core technique: looping through every link on a page, sending a HEAD request to each URL, and flagging anything that returns a 400+ status code. Broken links quietly hurt user experience and SEO, and most sites have more of them than owners realize.

The Fetch API makes this genuinely simple in modern browsers and Node.js 18+. HEAD requests fetch only the response headers, not the full page body, making bulk link checks fast and bandwidth-light.

Here's the working code for a browser console snippet, a full page scanner with timeout handling, and a Node.js script for checking an entire sitemap.

HEAD
the fastest HTTP method for checking link status without downloading full content
400+
status code threshold typically used to flag a link as broken
18+
Node.js version required for native Fetch API support without extra libraries
Advertisement
Advertisement

01Quick DevTools Console Snippet

Paste this directly into your browser's console while on any page to check every link instantly, then watch the Network tab for 404s or failed requests.

Console Snippet: Check All Links on Current Page
document.querySelectorAll('a[href]:not([href=""])').forEach(anchor => {
  fetch(anchor.href, { method: 'HEAD' })
    .then(res => {
      if (res.status >= 400) {
        console.warn('Broken link:', anchor.href, 'Status:', res.status);
      }
    })
    .catch(err => {
      console.error('Failed to fetch:', anchor.href, err);
    });
});

02Full Page Scanner With Timeout and Fallback

Some servers reject HEAD requests entirely or return inconsistent statuses. This version adds a timeout via AbortController and automatically falls back to a GET request when a server responds with 405 or 403.

Full Broken Link Checker (Browser or Node.js 18+)
async function checkLink(url, timeout = 10000) {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), timeout);

  try {
    let res = await fetch(url, {
      method: 'HEAD',
      signal: controller.signal,
      redirect: 'follow'
    });

    // Some servers reject HEAD, fall back to GET
    if (res.status === 405 || res.status === 403) {
      res = await fetch(url, {
        method: 'GET',
        signal: controller.signal,
        redirect: 'follow'
      });
    }

    clearTimeout(timer);
    return {
      url,
      status: res.status,
      ok: res.ok,
      redirected: res.redirected,
      finalUrl: res.url
    };
  } catch (err) {
    clearTimeout(timer);
    return {
      url,
      status: 0,
      ok: false,
      error: err.name === 'AbortError' ? 'timeout' : err.message
    };
  }
}

async function checkAllLinks() {
  const links = [...new Set(
    Array.from(document.querySelectorAll('a[href]'))
      .map(a => a.href)
      .filter(href => href.startsWith('http'))
  )];

  const results = await Promise.all(links.map(url => checkLink(url)));
  const broken = results.filter(r => !r.ok);

  console.table(broken);
  return broken;
}

checkAllLinks();
Advertisement
Advertisement

03Deduplicating Links Before Checking

Most pages contain repeated links, a link to the homepage often appears in the header, footer, and nav simultaneously. Checking each duplicate wastes requests. A native JavaScript Set removes duplicates automatically and lets you filter out mailto: and anchor-only links in the same pass.

Deduplicate and Filter Links Before Checking
const allLinkHrefs = Array.from(document.querySelectorAll('a[href]')).map(a => a.href);

const validHrefs = [...new Set(allLinkHrefs)].filter(href =>
  href &&
  !href.startsWith('mailto:') &&
  !href.includes('#')
);

console.log(`Checking ${validHrefs.length} unique links...`);

04Node.js Script for Checking a Full Sitemap

For checking an entire site rather than a single page, fetch the sitemap.xml, extract every URL, then loop through and check each one. This runs well as a scheduled script or CI job.

Node.js: Check Every URL in a Sitemap
import { parseStringPromise } from 'xml2js';

async function checkSitemap(sitemapUrl) {
  const response = await fetch(sitemapUrl);
  const xmlData = await response.text();
  const result = await parseStringPromise(xmlData);
  const urls = result.urlset.url.map(entry => entry.loc[0]);

  const broken = [];

  for (const url of urls) {
    try {
      const res = await fetch(url, { method: 'HEAD' });
      if (res.status >= 400) {
        broken.push({ url, status: res.status });
      }
    } catch (error) {
      broken.push({ url, status: 0, error: error.message });
    }
  }

  console.log(`Checked ${urls.length} URLs, found ${broken.length} broken.`);
  return broken;
}

checkSitemap('https://example.com/sitemap.xml');
Advertisement
Advertisement

05Approach Comparison

A quick reference for choosing the right approach based on your situation.

ApproachBest ForLimitation
Browser console snippetQuick, one-off checks on a live pageManual, doesn't scale across many pages
Full scanner with fallbackSingle-page audits with retry logicStill limited to one page at a time
Node.js sitemap scriptWhole-site checks, CI/CD, scheduled jobsRequires server-side execution environment
Headless browser (Playwright)SPAs where links are JS-rendered after loadHeavier setup, slower per-page execution

06Practical Notes Before Running These Scripts

A few things worth checking before running any of the above at scale.

Watch for CORS restrictions when checking cross-origin links directly from a browser console.

Always set a timeout, unresponsive servers can otherwise hang a batch check indefinitely.

Fall back to GET when HEAD is rejected, some servers return 403 or 405 for HEAD requests specifically.

Deduplicate links first, checking the same URL five times wastes requests and time.

For SPAs, use a headless browser instead of static HTML parsing, since JS-injected links won't appear otherwise.

07Common Questions

HEAD requests return only the response headers, not the full page body, making them faster and far less bandwidth-intensive for bulk link checking.

Some servers return 403 or 405 for HEAD requests specifically. The fix is falling back to a GET request only when that happens, rather than using GET for every check.

Not with static HTML parsing alone. For SPAs where navigation items are injected after page load, a headless browser tool like Playwright is needed to capture JS-rendered links.

It can, for cross-origin requests where the target server doesn't allow it. Server-side Node.js scripts avoid this limitation entirely since CORS is a browser-enforced restriction.

Fetch your sitemap.xml, parse out every URL listed, then loop through and check each one with a HEAD request, ideally as a scheduled Node.js script or CI job.

What We Learn Today

HEAD requests check status without downloading full content

AbortController adds timeout protection to prevent hanging checks

Fall back to GET only when a server rejects HEAD specifically

Deduplicating links with a Set avoids wasted repeat requests

Sitemap-based checking scales to whole-site audits

SPAs need a headless browser to capture JS-rendered links

Build a Complete Technical SEO Toolkit

Broken link detection pairs well with canonical tags and robots directives. Explore both guides next.

0 Votes: 0 Upvotes, 0 Downvotes (0 Points)

Leave a reply

Loading Next Post...
Search
Popular Now
Loading

Signing-in 3 seconds...

Signing-up 3 seconds...