Santaji GadeJavaScript, Development3 days ago15 Views

Detect broken links using JavaScript with working HEAD-request code console snippet, full scanner with retry logic, and a Node.js sitemap checker.
Table of Contents
ToggleDetect 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.
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.
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);
});
});
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.
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();
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.
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...`);
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.
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');
A quick reference for choosing the right approach based on your situation.
| Approach | Best For | Limitation |
|---|---|---|
| Browser console snippet | Quick, one-off checks on a live page | Manual, doesn't scale across many pages |
| Full scanner with fallback | Single-page audits with retry logic | Still limited to one page at a time |
| Node.js sitemap script | Whole-site checks, CI/CD, scheduled jobs | Requires server-side execution environment |
| Headless browser (Playwright) | SPAs where links are JS-rendered after load | Heavier setup, slower per-page execution |
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.
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.
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
Broken link detection pairs well with canonical tags and robots directives. Explore both guides next.









