The Hidden Trap of Headless Browsers: Why Can't Your Automation Tool Catch Early Page Errors?

Introduction

You’re debugging a frontend engineering issue — the page is behaving abnormally. You ask an AI to open the page with a browser tool and check the console for errors.

The AI opens the page, scans around, and tells you: The console is clean, no errors whatsoever.

You’re skeptical. You open Chrome DevTools yourself — three bright red errors are staring you in the face, the page has already crashed into a white screen. The AI visited the exact same page using a Headless browser, so why did it catch nothing?

This isn’t a bug in some particular tool, nor is it your configuration issue. It’s a timing trap buried deep in the design of Headless browsers that has driven countless developers to question their sanity over the past few years. It affects every modern frontend framework — React, Vue, SvelteKit, Next.js — none are spared.

The good news is, by 2026, all major tools have official solutions. But before we reveal the answer, let’s first understand one thing: how exactly does this error “disappear”?

Reproducing the Problem: The Vanishing Error

The Crime Scene

Everything looks normal:

  1. Running fine in local dev mode, building and deploying without issues
  2. Opening with regular Chrome, DevTools console clearly shows red errors
  3. But when accessing with Headless tools (older versions), something strange happens:
    • agent-browser console output — empty
    • Puppeteer’s page.on('pageerror')never triggered
    • Selenium’s log system — blank

The page has clearly crashed into a white screen, but these tools all unanimously tell you: “No errors.”

Note: Playwright v1.40+ has significantly improved this issue, catching most errors by default. The following analysis mainly addresses older version behavior.

It’s Not Your Fault, It’s a Tool Timing Flaw

The truth is surprisingly simple: All high-level Headless browser tool wrappers used to inject error capture code only after page load completed.

Yet modern frontend framework errors occur precisely during page loading — within the few hundred milliseconds of framework initialization, component mounting, and hydration. Your error catcher hasn’t even been installed yet when the error has already happened and vanished.

This is a widely discussed industry issue with thousands of related GitHub issues.

The Root Cause: An Unavoidable Timing Trap

Precise Timeline Analysis

Let’s break down the process millisecond by millisecond to understand why errors were forever uncatchable:

TimeEventOld Tool Error Capture StateNew Playwright State
T0Browser starts requesting HTMLNot initializedInitialized
T1HTML downloaded, parsing beginsNot initializedInitialized
T2Framework entry script parsed and executedNot initializedInitialized
T3Framework initialization starts (ReactDOM.render, createApp, kit.start(), etc.)Not initializedInitialized
T4Component initialization throws unhandled Promise rejection or sync errorNot initializedCapturing
T5Browser fires DOMContentLoaded eventNot initializedCapturing
T6Browser fires load eventNot initializedCapturing
T7Headless tool detects page load completeNot initializedCapturing
T8Headless tool injects error capture codeMissed errorsAlready captured
T9You run command to get errorsReturns emptyReturns full error list

Root Cause

To catch errors in Headless browsers, you must inject error listeners before page load, which requires directly calling the Chrome DevTools Protocol (CDP) Runtime.enable method.

Yet almost all high-level automation tool wrappers failed to call this method at the right time — until recently.

Which Frameworks and Tools Are Affected?

All Modern Frontend Frameworks

100% of modern frontend frameworks encounter this issue, though severity varies:

FrameworkAffected SeverityMost Common Early Error Type
SvelteKit⭐⭐⭐⭐⭐unhandled Promise rejection
Next.js (App Router)⭐⭐⭐⭐⭐hydration mismatch, server component errors
Nuxt.js 3⭐⭐⭐⭐⭐Nitro server errors, Vue render errors
React + Vite⭐⭐⭐⭐Component init errors, undefined env variables
Vue 3 + Vite⭐⭐⭐⭐setup function errors, third-party library incompatibility
Angular⭐⭐⭐Module loading errors, dependency injection failures
Solid.js⭐⭐⭐⭐Reactivity system errors, hydration errors
Astro⭐⭐⭐Island component loading errors, integration plugin errors

All Headless Browser Tools

Similarly, all automation tools based on Headless Chrome had this issue, but most are now fixed:

ToolAffected Severity (current)Official Solution AvailableFixed VersionNative Capture
agent-browser⭐⭐⭐⭐⭐✅ Perfect solution availableBuilds after Apr 17, 2026Weak
Playwright⭐⭐✅ Perfect solution availablev1.9+Strong (v1.40+ greatly improved)
Puppeteer⭐⭐⭐⭐✅ Perfect solution availablev1.0+Medium
Selenium⭐⭐⭐⭐⭐✅ Solution availablev4.0+Weak
Cypress⭐⭐✅ Solution availablev3.0+Medium
TestCafe⭐⭐⭐✅ Solution availablev1.18+Medium

Important note: Playwright currently has the strongest native error capture capability. Since v1.40, it enables the CDP Runtime domain immediately when creating a page, catching the vast majority of framework initialization errors, including SvelteKit and Next.js hydration errors.

Complete Official Solutions

The core idea behind all official solutions is consistent: Inject error capture scripts before page load. Below are the official recommended implementations for each tool.

agent-browser Official Solution

Officially confirmed: In PR #1257 merged on April 17, 2026, Vercel Labs officially added pre-navigation initialization script functionality to agent-browser, completely solving the early error capture problem.

The PR is titled “feat(react): React introspection, Web Vitals, and SPA primitives” and includes, besides React dev tools support, full startup lifecycle primitives — including the error capture functionality we need.

bash
1
2
# Load init script from file, execute before first navigation
agent-browser open --init-script ./error-capture.js http://localhost:5173
bash
1
2
3
4
5
# Start browser, add init script, then navigate
agent-browser batch \
  '["open"]' \
  '["addinitscript", "window.__earlyErrors = []; window.onerror = (m,s,l,c,e) => __earlyErrors.push({type:\"sync_error\",message:m,source:s,line:l,column:c,stack:e?.stack,timestamp:new Date().toISOString()}); window.addEventListener(\"unhandledrejection\", e => {e.preventDefault(); __earlyErrors.push({type:\"unhandled_promise_rejection\",reason:e.reason?.message||String(e.reason),stack:e.reason?.stack,timestamp:new Date().toISOString()});});"]' \
  '["navigate", "http://localhost:5173"]'

Method 3: Global Setting via Environment Variable

bash
1
2
3
# Applies to all agent-browser commands, supports multiple scripts separated by commas
export AGENT_BROWSER_INIT_SCRIPTS="./error-capture.js,./another-script.js"
agent-browser open http://localhost:5173

Method 4: Removing Added Init Scripts

bash
1
2
3
4
5
# List all added init scripts and their IDs
agent-browser listinitscripts

# Remove a specific init script by ID
agent-browser removeinitscript <identifier>

Complete error-capture.js File

javascript
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// Save as error-capture.js, place in project root
window.__earlyErrors = window.__earlyErrors || [];

// 1. Capture all sync errors
window.onerror = function(message, source, lineno, colno, error) {
  window.__earlyErrors.push({
    type: 'sync_error',
    message: message,
    source: source,
    line: lineno,
    column: colno,
    stack: error?.stack || new Error(message).stack,
    timestamp: new Date().toISOString()
  });
  return false; // Let browser also display errors in console
};

// 2. Capture all unhandled Promise rejections (most common in modern frameworks)
window.addEventListener('unhandledrejection', function(event) {
  event.preventDefault(); // Prevent default browser error prompt
  
  window.__earlyErrors.push({
    type: 'unhandled_promise_rejection',
    reason: event.reason?.message || String(event.reason),
    stack: event.reason?.stack || new Error(String(event.reason)).stack,
    timestamp: new Date().toISOString()
  });
});

// 3. Capture all console.error calls
const originalConsoleError = console.error;
console.error = function(...args) {
  window.__earlyErrors.push({
    type: 'console_error',
    args: args.map(arg => {
      try {
        return typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg);
      } catch (e) {
        return String(arg);
      }
    }),
    stack: new Error().stack,
    timestamp: new Date().toISOString()
  });
  
  originalConsoleError.apply(console, args);
};

// 4. Provide a utility function to print all errors
window.printEarlyErrors = function() {
  console.log('=== Captured Early Errors ===');
  window.__earlyErrors.forEach((err, index) => {
    console.log(`\nError ${index + 1} (${err.type}):`);
    console.log(`Time: ${err.timestamp}`);
    console.log(`Message: ${err.message || err.reason}`);
    if (err.stack) {
      console.log(`Stack:\n${err.stack}`);
    }
  });
  console.log('============================');
};

Retrieving Captured Errors

bash
1
2
3
4
5
# Get formatted JSON error list
agent-browser eval "JSON.stringify(window.__earlyErrors, null, 2)"

# Or print directly to console
agent-browser eval "window.printEarlyErrors()"

Version Requirements

  • Requires a version of agent-browser built on or after April 17, 2026
  • Check version with agent-browser --version
  • Upgrade command: agent-browser upgrade

Playwright Official Solution (Strongest Native Capability)

Playwright has provided a perfect solution since v1.9: the addInitScript() method. Since v1.40, its native error capture capability is already very powerful.

Official Documentation

“Script is evaluated after the document is created but before any of its scripts are run. This is useful to amend the JavaScript environment.”

Playwright official docs explicitly recommend using page.on('pageerror') to catch unhandled exceptions, and page.on('console') to catch console errors. For most modern framework hydration errors, these two events are sufficient.

Best Practice Implementation

javascript
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
const { chromium } = require('playwright');

async function captureAllErrors(url) {
  const browser = await chromium.launch({ headless: 'new' });
  const context = await browser.newContext();
  
  const allErrors = [];
  
  // ✅ Playwright native error capture (v1.40+ handles ~95% of cases)
  context.on('console', msg => {
    if (msg.type() === 'error') {
      allErrors.push({
        type: 'console_error',
        message: msg.text(),
        location: msg.location(),
        stack: msg.stackTrace(),
        timestamp: new Date().toISOString()
      });
    }
  });
  
  context.on('pageerror', error => {
    allErrors.push({
      type: 'page_error',
      message: error.message,
      stack: error.stack,
      timestamp: new Date().toISOString()
    });
  });
  
  // ⚠️ Supplement for extremely early errors (e.g., errors in document.write)
  // Only enable this if you confirm native events miss certain errors
  await context.addInitScript(`
    window.__earlyErrors = [];
    
    window.onerror = function(message, source, lineno, colno, error) {
      window.__earlyErrors.push({
        type: 'extreme_early_sync_error',
        message: message,
        source: source,
        line: lineno,
        column: colno,
        stack: error?.stack || new Error(message).stack,
        timestamp: new Date().toISOString()
      });
      return false;
    };
    
    window.addEventListener('unhandledrejection', function(event) {
      event.preventDefault();
      window.__earlyErrors.push({
        type: 'extreme_early_promise_rejection',
        reason: event.reason?.message || String(event.reason),
        stack: event.reason?.stack || new Error(String(event.reason)).stack,
        timestamp: new Date().toISOString()
      });
    });
  `);
  
  const page = await context.newPage();
  
  try {
    await page.goto(url, { waitUntil: 'networkidle0' });
  } catch (e) {
    allErrors.push({
      type: 'navigation_error',
      message: e.message,
      stack: e.stack,
      timestamp: new Date().toISOString()
    });
  }
  
  // Get extreme early errors (if addInitScript was enabled)
  const earlyErrors = await page.evaluate(() => window.__earlyErrors || []);
  
  await browser.close();
  
  // Merge all errors and sort by time
  return [...earlyErrors, ...allErrors].sort((a, b) => 
    new Date(a.timestamp) - new Date(b.timestamp)
  );
}

// Usage example
captureAllErrors('http://localhost:5173').then(errors => {
  console.log('All captured errors:');
  console.log(JSON.stringify(errors, null, 2));
  
  if (errors.length > 0) {
    console.log(`\nFound ${errors.length} errors total`);
    process.exit(1);
  } else {
    console.log('\nNo errors found');
    process.exit(0);
  }
});

Important Notes

  • Playwright v1.40+ users should prefer native events: page.on('pageerror') and context.on('console') already catch the vast majority of errors
  • addInitScript() is supplementary only: Only use when you confirm native events miss certain extreme early errors
  • Add listeners at the context level: This way all new pages inherit these listeners
  • Playwright also added page.console_messages() and page.page_errors() methods for direct access to historical messages

Puppeteer Official Solution

Puppeteer has provided the corresponding solution since its first version: the evaluateOnNewDocument() method.

Official Documentation

“The function is invoked upon the following scenarios: whenever the page is navigated; whenever the child frame is attached or navigated. The function is invoked after the document was created but before any of its scripts were run.”

Implementation Code

javascript
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
const puppeteer = require('puppeteer-core');

async function captureAllErrors(url) {
  const browser = await puppeteer.launch({
    headless: 'new',
    executablePath: '/usr/bin/google-chrome', // Adjust for your system
    args: ['--no-sandbox', '--disable-setuid-sandbox']
  });

  const page = await browser.newPage();
  
  // ✅ Key: add evaluation script before navigation
  await page.evaluateOnNewDocument(() => {
    window.__earlyErrors = [];
    
    window.onerror = function(message, source, lineno, colno, error) {
      window.__earlyErrors.push({
        type: 'sync_error',
        message: message,
        source: source,
        line: lineno,
        column: colno,
        stack: error?.stack || new Error(message).stack,
        timestamp: new Date().toISOString()
      });
      return false;
    };
    
    window.addEventListener('unhandledrejection', function(event) {
      event.preventDefault();
      window.__earlyErrors.push({
        type: 'unhandled_promise_rejection',
        reason: event.reason?.message || String(event.reason),
        stack: event.reason?.stack || new Error(String(event.reason)).stack,
        timestamp: new Date().toISOString()
      });
    });
  });
  
  // Also use CDP protocol to capture all errors (ultimate safeguard)
  const client = await page.createCDPSession();
  await client.send('Runtime.enable');
  
  const cdpErrors = [];

  client.on('Runtime.consoleAPICalled', event => {
    if (event.type === 'error') {
      cdpErrors.push({
        type: 'cdp_console_error',
        args: event.args.map(arg => arg.value || arg.description),
        stack: event.stackTrace?.callFrames || [],
        timestamp: new Date(event.timestamp).toISOString()
      });
    }
  });
  
  client.on('Runtime.exceptionThrown', event => {
    cdpErrors.push({
      type: 'cdp_exception',
      message: event.exceptionDetails.exception?.description || event.exceptionDetails.text,
      stack: event.exceptionDetails.stackTrace?.callFrames || [],
      timestamp: new Date().toISOString()
    });
  });
  
  client.on('Runtime.unhandledPromiseRejection', event => {
    cdpErrors.push({
      type: 'cdp_unhandled_rejection',
      reason: event.reason?.description || String(event.reason),
      timestamp: new Date().toISOString()
    });
  });

  try {
    await page.goto(url, { waitUntil: 'networkidle0' });
  } catch (e) {
    cdpErrors.push({
      type: 'navigation_error',
      message: e.message,
      stack: e.stack,
      timestamp: new Date().toISOString()
    });
  }
  
  // Get early errors
  const earlyErrors = await page.evaluate(() => window.__earlyErrors);
  
  await browser.close();

  // Merge all errors
  return [...earlyErrors, ...cdpErrors];
}

Selenium Official Solution

Selenium 4.0+ also provides similar functionality: DevTools API + Runtime.enable.

Implementation Code (Java)

java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.devtools.DevTools;
import org.openqa.selenium.devtools.v126.runtime.Runtime;
import org.openqa.selenium.devtools.v126.runtime.model.ConsoleAPICalled;
import org.openqa.selenium.devtools.v126.runtime.model.ExceptionThrown;

import java.util.ArrayList;
import java.util.List;

public class ErrorCapture {
    public static void main(String[] args) {
        ChromeOptions options = new ChromeOptions();
        options.addArguments("--headless=new");
        
        ChromeDriver driver = new ChromeDriver(options);
        DevTools devTools = driver.getDevTools();
        devTools.createSession();
        
        // Enable Runtime domain
        devTools.send(Runtime.enable());
        
        List<Object> allErrors = new ArrayList<>();
        
        // Capture console errors
        devTools.addListener(Runtime.consoleAPICalled(), event -> {
            if (event.getType().equals(ConsoleAPICalled.Type.ERROR)) {
                System.out.println("Console error: " + event.getArgs());
                allErrors.add(event);
            }
        });
        
        // Capture JavaScript exceptions
        devTools.addListener(Runtime.exceptionThrown(), event -> {
            ExceptionThrown.ExceptionDetails details = event.getExceptionDetails();
            System.out.println("Exception thrown: " + details.getException().get().getDescription());
            allErrors.add(event);
        });
        
        // Navigate to page
        driver.get("http://localhost:5173");
        
        driver.quit();
    }
}

Official docs: https://www.selenium.dev/documentation/webdriver/bidirectional/chrome_devtools/

Universal Solution: HTML Inline Script Injection (Last Line of Defense)

Although all tools now provide official solutions, adding an inline error capture script at the very top of your HTML remains the safest approach, applicable to any environment and any tool. This is also the standard approach used by error monitoring services like Sentry.

Implementation Principle

Inject a global error catcher before the browser parses any other code. This script executes at the very top of <head>, before any framework code.

Universal Implementation (Works for All Frameworks)

Edit your HTML entry file, this code must be placed at the very top of <head>:

html
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
<!DOCTYPE html>
<html lang="en">
<head>
  <!-- Global Early Error Catcher - MUST be before all other scripts -->
  <script>
    window.__earlyErrors = window.__earlyErrors || [];
    
    window.onerror = function(message, source, lineno, colno, error) {
      window.__earlyErrors.push({
        type: 'sync_error',
        message: message,
        source: source,
        line: lineno,
        column: colno,
        stack: error?.stack || new Error(message).stack,
        timestamp: new Date().toISOString()
      });
      return false;
    };
    
    window.addEventListener('unhandledrejection', function(event) {
      event.preventDefault();
      window.__earlyErrors.push({
        type: 'unhandled_promise_rejection',
        reason: event.reason?.message || String(event.reason),
        stack: event.reason?.stack || new Error(String(event.reason)).stack,
        timestamp: new Date().toISOString()
      });
    });
    
    const originalConsoleError = console.error;
    console.error = function(...args) {
      window.__earlyErrors.push({
        type: 'console_error',
        args: args.map(arg => {
          try {
            return typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg);
          } catch (e) {
            return String(arg);
          }
        }),
        stack: new Error().stack,
        timestamp: new Date().toISOString()
      });
      originalConsoleError.apply(console, args);
    };
  </script>

  <!-- Original content below -->
  <meta charset="utf-8" />
  <link rel="icon" href="/favicon.png" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <!-- Framework injected content -->
</head>
<body>
  <!-- App content -->
</body>
</html>

Placement by Framework

  • SvelteKit: src/app.html
  • Next.js: Top of app/layout.tsx, before any imports
  • Nuxt.js: Before <script setup> in app.vue, or use nuxt.config.ts app.head.script configuration
  • React + Vite: index.html
  • Vue + Vite: index.html

Framework-Level Global Error Handling (Second Line of Defense)

As a second line of defense, you should add global error handling in each framework:

SvelteKit

Add in src/hooks.client.js:

javascript
1
2
3
4
export function handleError({ error, event }) {
  console.error('SvelteKit global error:', error);
  // Report to log system
}

Official docs: https://kit.svelte.dev/docs/hooks#shared-hooks-handleerror

Next.js

Add in app/global-error.tsx:

javascript
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
'use client';

export default function GlobalError({ error, reset }) {
  console.error('Next.js global error:', error);
  // Report to log system
  
  return (
    <html>
      <body>
        <h2>Something went wrong!</h2>
        <button onClick={() => reset()}>Try again</button>
      </body>
    </html>
  );
}

Official docs: https://nextjs.org/docs/app/building-your-application/routing/error-handling#global-error-handling

Nuxt.js

Add in plugins/error-handler.ts:

typescript
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.vueApp.config.errorHandler = (error, instance, info) => {
    console.error('Vue global error:', error, info);
    // Report to log system
  };

  nuxtApp.hook('vue:error', (error, instance, info) => {
    console.error('Nuxt Vue error:', error, info);
    // Report to log system
  });
});

Official docs: https://nuxt.com/docs/getting-started/error-handling#vue-errors

React

javascript
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class ErrorBoundary extends React.Component {
  componentDidCatch(error, errorInfo) {
    console.error('React ErrorBoundary caught error:', error, errorInfo);
    // Report to log system
  }

  render() {
    return this.props.children;
  }
}

Official docs: https://react.dev/reference/react/Component#componentdidcatch

Vue 3

javascript
1
2
3
4
5
6
const app = createApp(App);

app.config.errorHandler = (err, instance, info) => {
  console.error('Vue global error:', err, info);
  // Report to log system
};

Official docs: https://vuejs.org/api/application.html#app-config-errorhandler

Quick Debugging Tips

If you just want to quickly locate an issue without modifying code, try these methods:

Run in Non-headless Mode

bash
1
2
3
4
5
6
7
8
# agent-browser
agent-browser run --headless=false http://localhost:5173

# Puppeteer
const browser = await puppeteer.launch({ headless: false });

# Playwright
const browser = await chromium.launch({ headless: false });

Check Build Output

Many early errors can be found during build:

bash
1
2
3
4
5
# Build project and check warnings and errors
npm run build

# Preview build results (exposes many issues hidden in dev mode)
npm run preview

Enable Chrome Detailed Logging

bash
1
2
# Add these args when starting Chrome
chrome --headless=new --enable-logging --v=1 --log-level=0 http://localhost:5173

Common Early Error Causes

These are errors that all frameworks encounter during initialization that are easy to throw and hard to catch:

  1. Undefined environment variables: import.meta.env.VITE_XXX or process.env.NEXT_PUBLIC_XXX doesn’t exist on the client
  2. SSR incompatibility: Third-party libraries accessing window or document during server-side rendering
  3. Data fetching errors: load functions, getServerSideProps, or API calls in server components failing
  4. Hydration mismatches: Server-rendered HTML not matching client-generated DOM
  5. Static resource path errors: Incorrect path configuration for static assets after build
  6. Module loading failures: JS chunk loading failures or third-party CDN unavailability
  7. Dependency version incompatibility: Version conflicts between different dependencies

Best Practices

Choose the right approach for your tool

  • agent-browser users: Use the official --init-script parameter or addinitscript command
  • Playwright users: Prefer native page.on('pageerror') and context.on('console'), only add addInitScript() if you confirm omissions
  • Puppeteer users: Use page.evaluateOnNewDocument()
  • Selenium users: Use DevTools API + Runtime.enable

Combine with CDP protocol as ultimate safeguard

For the most complex cases, directly use Chrome DevTools Protocol’s Runtime.enable method, which catches all possible errors.

Still recommend inline script in HTML as last line of defense

Although tools now provide solutions, adding an inline error capture script at the very top of HTML remains the safest approach for any environment.

Verify tool version meets requirements

All major tools have solved this issue, especially Playwright v1.40+ and agent-browser (post-April 2026), with significant improvements in native error capture capability.

Add framework-level global error handling

As a second line of defense, catch any errors that might slip through.

Conclusion

The good news is: this “invisible error” problem that once plagued countless developers now has perfect official solutions.

  • agent-browser completely solved this issue in PR #1257 on April 17, 2026, adding full pre-navigation init script support
  • Playwright not only provided the solution early, but also greatly improved native capture capability in v1.40+, making it the best-performing tool overall
  • Puppeteer and Selenium also have mature solutions
  • The core idea behind all solutions is: inject error capture scripts before page load

Now you no longer need to worry about “page white screen but console empty.” Configure using the official methods above, and you’ll be able to catch all errors occurring during page initialization.

If you’ve encountered similar “invisible errors,” feel free to share your experience in the comments.

References

[1] GitHub Issue: “Puppeteer page.on(‘pageerror’) not catching errors during page load” https://github.com/puppeteer/puppeteer/issues/1933

[2] Chrome DevTools Protocol: Runtime Domain https://chromedevtools.github.io/devtools-protocol/tot/Runtime/

[3] Playwright Official Documentation: Page Errors https://playwright.dev/docs/api/class-page#page-event-page-error

[4] agent-browser PR #1257: feat(react): React introspection, Web Vitals, and SPA primitives https://github.com/vercel-labs/agent-browser/pull/1257

[5] Playwright Official Documentation: addInitScript https://playwright.dev/docs/api/class-browsercontext#browser-context-add-init-script

[6] Playwright Release Notes: https://playwright.dev/docs/release-notes

[7] Puppeteer Official Documentation: evaluateOnNewDocument https://pptr.dev/api/core.page.evaluateonnewdocument

[8] Selenium Official Documentation: Chrome DevTools https://www.selenium.dev/documentation/webdriver/bidirectional/chrome_devtools/

[9] Sentry Documentation: Install for Browser JavaScript https://docs.sentry.io/platforms/javascript/install/

[10] SvelteKit Official Documentation: Hooks https://kit.svelte.dev/docs/hooks

[11] Next.js Official Documentation: Error Handling https://nextjs.org/docs/app/building-your-application/routing/error-handling

[12] Nuxt Official Documentation: Error Handling https://nuxt.com/docs/getting-started/error-handling

[13] React Official Documentation: Error Boundaries https://react.dev/reference/react/Component#componentdidcatch

[14] Vue Official Documentation: Error Handling https://vuejs.org/api/application.html#app-config-errorhandler