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

// 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

  1. Step 1.

    Start the page under test. Stop it with Ctrl-C when you are finished.

    node probe-server.mjs
    
    page 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.

  2. Step 2.

    Record every message with its level. Save this as console-capture.mjs and run node 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 messages

    Two of the eight are pageerror, the event for an exception nobody caught. One is a console.warn call a developer wrote. Three are 404s, and not one of the three names the URL that failed.

  3. 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.js

    The 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.

  4. 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 listener

    Five of the eight. The listener carries the URL for each broken resource, which the console text did not. It misses the console.error call, the console.warn call 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

Sign: The 404 lines in the console do not say which URL failed.Cause: Chrome puts the URL in the message location, not in its text. A driver that records only the message text loses it, as step 2 shows. Record response statuses in the same run, which is how the three 404s here were named, one of them a favicon request the page never wrote.
Sign: An error-reporting tool reports fewer errors than the console shows.Cause: Eight console messages here, five caught by an error listener. A listener never receives a console.error call, never receives a console.warn, and never receives a request Chrome made on its own behalf. Neither source is wrong, so quote the number together with the method that produced it.
Sign: The console is empty after a reload that you know produced errors.Cause: Chrome clears the console on navigation unless Preserve log is ticked in the Console settings, and messages logged before DevTools opened are not replayed. Open DevTools, tick Preserve log, then reload.
Sign: A refused script appears as a failed request with no error text.Cause: The CSP refusal on this page arrives in requestfailed with an errorText of undefined, because no network attempt happened. The console line is the only place that names the directive that refused it.

What to check next

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.

basic7 minpublished updated Maks Verny