How to check console errors on a website
Press F12, open the Console tab, tick Preserve log, and reload. Chrome lists every message with its level. To record the same list in CI, drive Chrome with puppeteer-core and collect the console and pageerror events. A deliberately broken test page produced eight messages: five errors, one warning, two uncaught exceptions.
Why check this
Console output is the cheapest regression signal a browser gives you, and it is the first thing to read after a deploy to staging, after a dependency bump, and after any change to a Content Security Policy. A page that renders correctly can still be logging a refused script, a rejected promise and three failed requests.
The failure it catches is the one nobody reports. A consent banner script gets blocked by a new CSP directive, the banner never renders, and the analytics that depend on consent go silent. Nothing looks wrong on screen. The console says so on line one.
Prerequisites
- Chrome. This page used version 152.0.7977.76 on Windows. The figures below are one capture on one machine on 2026-09-11.
- Node 22 or later and
puppeteer-core, installed withnpm i puppeteer-core. It drives the Chrome you already have and downloads no browser of its own. See the puppeteer API. - The Chrome path in each script below is the Windows default. On macOS it is
/Applications/Google Chrome.app/Contents/MacOS/Google Chrome. - Save this file as
probe-server.mjs. It serves one page with five script tags: one that works, one that returns 404, one that throws, one that a CSP directive refuses, and one on a second origin. It also serves a missing image.
// probe-server.mjs run: node probe-server.mjs stop: Ctrl-C
import http from 'node:http';
const page = `<!doctype html><meta charset="utf-8"><title>script probe</title>
<script src="/app.js"></script>
<script src="/missing.js"></script>
<script src="/boom.js"></script>
<script src="http://127.0.0.1:8811/blocked.js"></script>
<script src="http://127.0.0.1:8812/cdn.js"></script>
<template><script src="/in-template.js"></script></template>
<h1>script probe</h1><img src="/logo.png" alt="missing logo">`;
const files = {
'/': ['text/html', page],
'/app.js': ['text/javascript', `console.warn('app.js: cart module is deprecated');
var s = document.createElement('script'); s.src = '/injected.js'; document.head.appendChild(s);
Promise.reject(new Error('app.js: session fetch rejected'));
console.error('app.js: analytics key missing');
window.appRan = true;`],
'/boom.js': ['text/javascript', `throw new Error('boom.js: cannot read config of undefined');
window.boomRan = true;`],
'/injected.js': ['text/javascript', `window.injectedRan = true;`],
'/in-template.js': ['text/javascript', `window.templateRan = true;`],
'/blocked.js': ['text/javascript', `window.blockedRan = true;`],
};
http.createServer((req, res) => {
const f = files[req.url];
if (!f) { res.writeHead(404, { 'content-type': 'text/plain' }); return res.end('not found'); }
const h = { 'content-type': f[0] };
if (req.url === '/') h['content-security-policy'] = "script-src 'self' http://127.0.0.1:8812";
res.writeHead(200, h);
res.end(f[1]);
}).listen(8811, () => console.log('page on http://localhost:8811/'));
// A second origin, standing in for a CDN, with resource timing opened up.
http.createServer((req, res) => {
res.writeHead(200, { 'content-type': 'text/javascript', 'timing-allow-origin': '*' });
res.end('window.cdnRan = true;');
}).listen(8812, () => console.log('cdn on http://127.0.0.1:8812/'));
Steps
- Step 1.
Start the page under test. Stop it with Ctrl-C when you are finished.
node probe-server.mjspage on http://localhost:8811/ cdn on http://127.0.0.1:8812/Open
http://localhost:8811/in Chrome, press F12, select the Console tab, tick Preserve log in the Console settings, and reload. The list on screen is the list the next step records. - Step 2.
Record every message with its level. Save this as
console-capture.mjsand runnode console-capture.mjs.// console-capture.mjs run: node console-capture.mjs import { launch } from 'puppeteer-core'; const CHROME = 'C:/Program Files/Google/Chrome/Application/chrome.exe'; const browser = await launch({ executablePath: CHROME, headless: true }); const page = await browser.newPage(); const log = []; page.on('console', (m) => log.push(`${m.type()}: ${m.text()}`)); page.on('pageerror', (e) => log.push(`pageerror: ${e.message}`)); await page.goto('http://localhost:8811/', { waitUntil: 'networkidle2' }); log.forEach((l) => console.log(l)); console.log('--- ' + log.length + ' messages'); await browser.close();error: Failed to load resource: the server responded with a status of 404 (Not Found) warn: app.js: cart module is deprecated error: app.js: analytics key missing pageerror: app.js: session fetch rejected pageerror: boom.js: cannot read config of undefined error: Loading the script 'http://127.0.0.1:8811/blocked.js' violates the following Content Security Policy directive: "script-src 'self' http://127.0.0.1:8812". Note that 'script-src-elem' was not explicitly set, so 'script-src' is used as a fallback. The action has been blocked. error: Failed to load resource: the server responded with a status of 404 (Not Found) error: Failed to load resource: the server responded with a status of 404 (Not Found) --- 8 messagesTwo of the eight are
pageerror, the event for an exception nobody caught. One is aconsole.warncall a developer wrote. Three are 404s, and not one of the three names the URL that failed. - Step 3.
Recover the URLs behind those 404 lines, and list the requests Chrome abandoned. Save this as
failed-requests.mjs.// failed-requests.mjs run: node failed-requests.mjs import { launch } from 'puppeteer-core'; const CHROME = 'C:/Program Files/Google/Chrome/Application/chrome.exe'; const browser = await launch({ executablePath: CHROME, headless: true }); const page = await browser.newPage(); const bad = [], failed = []; page.on('response', (r) => { if (r.status() >= 400) bad.push(`${r.status()} ${r.request().resourceType()} ${r.url()}`); }); page.on('requestfailed', (r) => failed.push(`${r.failure()?.errorText} ${r.url()}`)); await page.goto('http://localhost:8811/', { waitUntil: 'networkidle2' }); console.log('responses with a 4xx or 5xx status:'); bad.forEach((b) => console.log(' ' + b)); console.log('requests the browser could not complete:'); failed.forEach((f) => console.log(' ' + f)); await browser.close();responses with a 4xx or 5xx status: 404 script http://localhost:8811/missing.js 404 image http://localhost:8811/logo.png 404 other http://localhost:8811/favicon.ico requests the browser could not complete: undefined http://127.0.0.1:8811/blocked.js net::ERR_ABORTED http://localhost:8811/missing.jsThe third 404 is
/favicon.ico, which the page never asked for. Chrome requests it on its own, and it counts as a console error like any other. - Step 4.
Compare the console against an in-page listener, the kind an error-reporting snippet installs. Save this as
inpage-listener.mjs.// inpage-listener.mjs run: node inpage-listener.mjs import { launch } from 'puppeteer-core'; const CHROME = 'C:/Program Files/Google/Chrome/Application/chrome.exe'; const browser = await launch({ executablePath: CHROME, headless: true }); const page = await browser.newPage(); await page.evaluateOnNewDocument(() => { window.__errs = []; addEventListener('error', (e) => window.__errs.push( e.message ? 'js: ' + e.message : 'resource: ' + (e.target.src || e.target.href)), true); addEventListener('unhandledrejection', (e) => window.__errs.push('rejection: ' + e.reason.message)); }); await page.goto('http://localhost:8811/', { waitUntil: 'networkidle2' }); const errs = await page.evaluate(() => window.__errs); errs.forEach((e) => console.log(e)); console.log('--- ' + errs.length + ' caught by the listener'); await browser.close();rejection: app.js: session fetch rejected resource: http://localhost:8811/missing.js js: Uncaught Error: boom.js: cannot read config of undefined resource: http://127.0.0.1:8811/blocked.js resource: http://localhost:8811/logo.png --- 5 caught by the listenerFive of the eight. The listener carries the URL for each broken resource, which the console text did not. It misses the
console.errorcall, theconsole.warncall and the favicon request, because none of those is an error event.
How to read the result
| What you see | What it means | What to do |
| --- | --- | --- |
| pageerror, or Uncaught in the Console | An exception reached the top of the stack | Read the stack frame. Everything after that line in the same file never ran. |
| Failed to load resource ... 404 | A subresource is missing | Take the URL from the response listener in step 3, not from the message text. |
| A message naming a Content Security Policy directive | Chrome refused the resource before requesting it | Compare the source list against the host in the message. Nothing reached the server. |
| Unhandled promise rejection | An async call failed with no catch | Treat it as an error. It never reaches window.onerror, and only unhandledrejection sees it. |
| An empty console | Either the page is clean or the log was cleared on navigation | Tick Preserve log and reload before believing it. |
Common mistakes
What to check next
- How to check if a javascript file is loaded: the same page read as a list of scripts, and why a console error is not the whole answer.
- How to check which third party scripts a page loads: most console noise on a real site comes from code the team did not write.
- How to check CSP header: the directive named in a refusal message, read from the response.
- How to check if mixed content exists on a page: another class of browser refusal that only the console reports.
- How to check number of requests on a page: the request list the response listener in step 3 reads from.
FAQ
What are console errors?
Messages Chrome logs at error level. They come from three sources: exceptions nobody caught, console.error calls a developer wrote, and resources the browser could not load or was told to refuse. Only the first kind stops code from running.
How do I check the browser console for errors in Chrome?
Press F12 or Ctrl-Shift-J, select the Console tab, tick Preserve log, and reload. Use the level filter to show Errors only. Click the file reference on the right of a message to open the line that produced it.
How do I check a website for JavaScript errors without opening DevTools?
Run the script in step 2. It launches Chrome headless, loads the URL and prints every message, so it fits inside a CI job. An in-page listener like step 4 works too, and it reports less.
Does a console error always mean the page is broken?
No. A missing favicon and a blocked analytics beacon both log errors while the page works. Sort by source: exceptions first, then refused resources, then console.error calls.
Verified
Verified by Maks VernyChrome 152.0.7977.76node 22.23.2puppeteer-core 25.10.0
Each output block is what the command above it printed on that date, on the host named in the step. Figures read from a live site move between runs. Compare the shape of the answer rather than the digits, and see the methodology for how a page is re-verified.
Related on this site
basic7 minpublished updated Maks Verny