Headless浏览器的隐形陷阱:为什么你的自动化工具抓不到页面早期错误?

前言

你在调试一个前端工程问题,页面表现异常。你让 AI 用浏览器工具帮你打开页面,检查控制台有没有报错。

AI 打开页面,扫了一圈,告诉你:控制台干干净净,没有任何错误。

你将信将疑,自己用 Chrome 打开 DevTools——三条鲜红的报错赫然在列,页面其实已经白屏崩溃了。AI 明明用 Headless 浏览器访问了同一个页面,为什么什么都没抓到?

这不是某个工具的bug,也不是你的配置问题。这是一个藏在Headless浏览器设计深处的时序陷阱,过去几年里让无数开发者怀疑人生。它影响所有现代前端框架——React、Vue、SvelteKit、Next.js,无一幸免。

好消息是,到了2026年,所有主流工具都已经给出了官方解决方案。但在揭开答案之前,先让我们搞清楚一件事:这个错误到底是怎么"消失"的?

问题重现:那个消失的错误

案发现场

一切看起来都正常:

  1. 本地开发模式运行良好,构建部署也没问题
  2. 用普通Chrome打开,DevTools控制台清晰地显示着红色错误
  3. 但用Headless工具访问时(旧版本),诡异的事情发生了:
    • agent-browser console 输出——空的
    • Puppeteer的 page.on('pageerror') ——没触发
    • Selenium的日志系统——一片空白

页面明明已经白屏崩溃,但这些工具异口同声地告诉你:“没有错误。”

注意:Playwright v1.40+ 已大幅改进此问题,默认会捕获大多数错误。以下分析主要针对旧版本行为。

不是你的错,是工具的时序缺陷

真相出人意料地简单:所有高层封装的Headless浏览器工具,过去都把错误捕获代码放在了页面加载完成之后才注入。

而现代前端框架的错误,恰恰发生在页面加载过程中——在框架初始化、组件挂载、hydration的几百毫秒里。你的错误捕获器还没就位,错误已经发生又消失了。

这是一个被广泛讨论的行业问题,在GitHub上有数千个相关issue。

问题本质:无法逾越的时序陷阱

精确的时间线分析

让我们把整个过程拆解到毫秒级,你就会明白为什么错误曾经永远抓不到:

时间点发生的事件旧版工具的错误捕获系统状态新版Playwright状态
T0浏览器开始请求HTML未初始化已初始化
T1HTML下载完成,开始解析未初始化已初始化
T2解析到框架入口脚本并开始执行未初始化已初始化
T3框架初始化开始(ReactDOM.render、createApp、kit.start()等)未初始化已初始化
T4组件初始化时抛出未处理的Promise拒绝或同步错误未初始化正在捕获
T5浏览器触发DOMContentLoaded事件未初始化正在捕获
T6浏览器触发load事件未初始化正在捕获
T7Headless工具检测到页面加载完成未初始化正在捕获
T8Headless工具注入错误捕获代码已错过错误已捕获完成
T9你执行命令获取错误返回空返回完整错误列表

根本原因

Headless浏览器的错误捕获必须在页面加载前注入监听器,这需要直接操作Chrome DevTools Protocol (CDP)的Runtime.enable方法。

几乎所有高层封装的自动化工具,都没有在正确的时机调用这个方法——直到最近。

哪些框架和工具会受影响?

所有现代前端框架

100%的现代前端框架都会遇到这个问题,只是严重程度不同:

框架受影响程度最常见的早期错误类型
SvelteKit⭐⭐⭐⭐⭐unhandled Promise rejection
Next.js (App Router)⭐⭐⭐⭐⭐hydration mismatch、server component错误
Nuxt.js 3⭐⭐⭐⭐⭐Nitro服务器错误、Vue渲染错误
React + Vite⭐⭐⭐⭐组件初始化错误、环境变量未定义
Vue 3 + Vite⭐⭐⭐⭐setup函数错误、第三方库不兼容
Angular⭐⭐⭐模块加载错误、依赖注入失败
Solid.js⭐⭐⭐⭐响应式系统错误、hydration错误
Astro⭐⭐⭐岛屿组件加载错误、集成插件错误

所有Headless浏览器工具

同样,所有基于Headless Chrome的自动化工具都曾有这个问题,但现在大部分都已修复:

工具受影响程度(当前版本)官方是否提供解决方案修复版本原生捕获能力
agent-browser⭐⭐⭐⭐⭐✅ 已提供完美解决方案2026年4月17日后构建版
Playwright⭐⭐✅ 已提供完美解决方案v1.9+强(v1.40+大幅改进)
Puppeteer⭐⭐⭐⭐✅ 已提供完美解决方案v1.0+
Selenium⭐⭐⭐⭐⭐✅ 已提供解决方案v4.0+
Cypress⭐⭐✅ 已提供解决方案v3.0+
TestCafe⭐⭐⭐✅ 已提供解决方案v1.18+

重要说明:Playwright是目前原生错误捕获能力最强的工具。从v1.40版本开始,它默认会在页面创建时立即启用CDP Runtime域,能够捕获绝大多数框架初始化阶段的错误,包括SvelteKit和Next.js的hydration错误。

官方解决方案大全

所有官方解决方案的核心思想都是一致的:在页面加载前注入错误捕获脚本。下面是各个工具的官方推荐实现方式。

agent-browser 官方解决方案

官方确认:在2026年4月17日合并的PR #1257中,Vercel Labs正式为agent-browser添加了预导航初始化脚本功能,彻底解决了早期错误捕获问题。

该PR标题为"feat(react): React introspection, Web Vitals, and SPA primitives",除了React开发工具支持外,还包含了完整的启动生命周期原语,其中就包括我们需要的错误捕获功能。

方法1:使用--init-script参数(推荐)

bash
1
2
# 从文件加载初始化脚本,在第一次导航前执行
agent-browser open --init-script ./error-capture.js http://localhost:5173

方法2:使用batch命令(推荐用于复杂场景)

bash
1
2
3
4
5
# 先启动浏览器,再添加初始化脚本,最后导航
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"]'

方法3:使用环境变量全局设置

bash
1
2
3
# 对所有agent-browser命令生效,支持多个脚本用逗号分隔
export AGENT_BROWSER_INIT_SCRIPTS="./error-capture.js,./another-script.js"
agent-browser open http://localhost:5173

方法4:移除已添加的初始化脚本

bash
1
2
3
4
5
# 查看所有已添加的初始化脚本及其ID
agent-browser listinitscripts

# 移除指定ID的初始化脚本
agent-browser removeinitscript <identifier>

完整的error-capture.js文件

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
// 保存为error-capture.js,放在项目根目录
window.__earlyErrors = window.__earlyErrors || [];

// 1. 捕获所有同步错误
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; // 让浏览器也能在控制台显示错误
};

// 2. 捕获所有未处理的Promise拒绝(现代框架最常见)
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()
  });
});

// 3. 捕获所有console.error调用
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. 提供一个打印所有错误的工具函数
window.printEarlyErrors = function() {
  console.log('=== 捕获到的早期错误 ===');
  window.__earlyErrors.forEach((err, index) => {
    console.log(`\n错误 ${index + 1} (${err.type}):`);
    console.log(`时间: ${err.timestamp}`);
    console.log(`消息: ${err.message || err.reason}`);
    if (err.stack) {
      console.log(`堆栈:\n${err.stack}`);
    }
  });
  console.log('========================');
};

获取捕获到的错误

bash
1
2
3
4
5
# 获取格式化的JSON错误列表
agent-browser eval "JSON.stringify(window.__earlyErrors, null, 2)"

# 或者直接打印到控制台
agent-browser eval "window.printEarlyErrors()"

版本要求

  • 需要使用2026年4月17日或之后构建的agent-browser版本
  • 可以通过agent-browser --version检查版本
  • 升级命令:agent-browser upgrade

Playwright 官方解决方案(原生能力最强)

Playwright从v1.9版本开始就提供了完美的解决方案:addInitScript()方法。从v1.40版本开始,它的原生错误捕获能力已经非常强大。

官方说明

“脚本在文档创建后但在任何脚本运行之前进行评估。这对于修改JavaScript环境非常有用。”

Playwright官方文档明确建议:使用page.on('pageerror')来捕获未处理的异常,使用page.on('console')来捕获控制台错误。对于大多数现代框架的hydration错误,这两个事件已经足够。

最佳实践实现

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原生错误捕获(v1.40+ 已足够应对95%的情况)
  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()
    });
  });
  
  // ⚠️ 仅针对极端早期错误的补充(如document.write中的错误)
  // 如果你发现原生事件确实错过了某些错误,再启用这部分
  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()
    });
  }
  
  // 获取极端早期错误(如果启用了addInitScript)
  const earlyErrors = await page.evaluate(() => window.__earlyErrors || []);
  
  await browser.close();
  
  // 合并所有错误并按时间排序
  return [...earlyErrors, ...allErrors].sort((a, b) => 
    new Date(a.timestamp) - new Date(b.timestamp)
  );
}

// 使用示例
captureAllErrors('http://localhost:5173').then(errors => {
  console.log('捕获到的所有错误:');
  console.log(JSON.stringify(errors, null, 2));
  
  if (errors.length > 0) {
    console.log(`\n总共发现 ${errors.length} 个错误`);
    process.exit(1);
  } else {
    console.log('\n没有发现任何错误');
    process.exit(0);
  }
});

重要注意事项

  • Playwright v1.40+ 用户优先使用原生事件page.on('pageerror')context.on('console')已经能够捕获绝大多数错误
  • addInitScript()仅作为补充:只有当你确认原生事件确实错过了某些极端早期错误时才需要使用
  • 建议在context级别添加监听器:这样所有新页面都会继承这些监听器
  • Playwright还新增了page.console_messages()page.page_errors()方法,可以直接获取历史消息

Puppeteer 官方解决方案

Puppeteer从第一个版本开始就提供了对应的解决方案:evaluateOnNewDocument()方法

官方说明

“该函数在以下情况之一被调用:每当页面导航时;每当子框架被附加或导航时。函数在文档创建后但在任何页面脚本运行之前被调用。”

实现代码

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', // 根据你的系统调整
    args: ['--no-sandbox', '--disable-setuid-sandbox']
  });

  const page = await browser.newPage();
  
  // ✅ 关键:在导航之前添加评估脚本
  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()
      });
    });
  });
  
  // 同时使用CDP协议捕获所有错误(终极保障)
  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()
    });
  }
  
  // 获取早期错误
  const earlyErrors = await page.evaluate(() => window.__earlyErrors);
  
  await browser.close();
  
  // 合并所有错误
  return [...earlyErrors, ...cdpErrors];
}

Selenium 官方解决方案

Selenium 4.0及以上版本也提供了类似的功能:DevTools API + Runtime.enable

实现代码(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();
        
        // 启用Runtime域
        devTools.send(Runtime.enable());
        
        List<Object> allErrors = new ArrayList<>();
        
        // 捕获控制台错误
        devTools.addListener(Runtime.consoleAPICalled(), event -> {
            if (event.getType().equals(ConsoleAPICalled.Type.ERROR)) {
                System.out.println("Console error: " + event.getArgs());
                allErrors.add(event);
            }
        });
        
        // 捕获JavaScript异常
        devTools.addListener(Runtime.exceptionThrown(), event -> {
            ExceptionThrown.ExceptionDetails details = event.getExceptionDetails();
            System.out.println("Exception thrown: " + details.getException().get().getDescription());
            allErrors.add(event);
        });
        
        // 导航到页面
        driver.get("http://localhost:5173");
        
        driver.quit();
    }
}

官方文档:https://www.selenium.dev/documentation/webdriver/bidirectional/chrome_devtools/

通用解决方案:HTML内联脚本注入(最后一道防线)

虽然现在所有工具都已经提供了官方解决方案,但在HTML最顶部添加内联错误捕获脚本仍然是最保险的做法,适用于任何环境任何工具。这也是Sentry等错误监控服务采用的标准做法。

实现原理

在浏览器解析任何其他代码之前,先注入一个全局错误捕获器。这个脚本会在<head>的最顶部执行,比任何框架代码都早。

通用实现(适用于所有框架)

编辑你的HTML入口文件,必须把这段代码放在<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>
  <!-- 全局早期错误捕获器 - 必须放在所有其他脚本之前 -->
  <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>

  <!-- 以下是原来的内容 -->
  <meta charset="utf-8" />
  <link rel="icon" href="/favicon.png" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <!-- 框架注入的内容 -->
</head>
<body>
  <!-- 应用内容 -->
</body>
</html>

各框架的具体放置位置

  • SvelteKitsrc/app.html
  • Next.jsapp/layout.tsx的最顶部,在任何导入之前
  • Nuxt.jsapp.vue<script setup>之前,或使用nuxt.config.tsapp.head.script配置
  • React + Viteindex.html
  • Vue + Viteindex.html

各框架的全局错误处理(第二道防线)

作为第二道防线,你应该在每个框架中添加全局错误处理:

SvelteKit

src/hooks.client.js中添加:

javascript
1
2
3
4
export function handleError({ error, event }) {
  console.error('SvelteKit全局错误:', error);
  // 上报到日志系统
}

官方文档:https://kit.svelte.dev/docs/hooks#shared-hooks-handleerror

Next.js

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全局错误:', error);
  // 上报到日志系统
  
  return (
    <html>
      <body>
        <h2>Something went wrong!</h2>
        <button onClick={() => reset()}>Try again</button>
      </body>
    </html>
  );
}

官方文档:https://nextjs.org/docs/app/building-your-application/routing/error-handling#global-error-handling

Nuxt.js

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全局错误:', error, info);
    // 上报到日志系统
  };

  nuxtApp.hook('vue:error', (error, instance, info) => {
    console.error('Nuxt Vue错误:', error, info);
    // 上报到日志系统
  });
});

官方文档: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错误边界捕获到错误:', error, errorInfo);
    // 上报到日志系统
  }

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

官方文档: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全局错误:', err, info);
  // 上报到日志系统
};

官方文档:https://vuejs.org/api/application.html#app-config-errorhandler

临时调试技巧

如果你只是想快速定位问题,不想修改代码,可以试试这些方法:

使用非headless模式运行

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 });

检查构建输出

很多早期错误在构建阶段就能发现:

bash
1
2
3
4
5
# 构建项目并检查警告和错误
npm run build

# 预览构建结果(这会暴露很多开发模式下看不到的问题)
npm run preview

启用Chrome的详细日志

bash
1
2
# 启动Chrome时添加这些参数
chrome --headless=new --enable-logging --v=1 --log-level=0 http://localhost:5173

常见的早期错误原因

这些是所有框架都会遇到的、最容易在初始化阶段抛出且难以捕获的错误:

  1. 环境变量未定义import.meta.env.VITE_XXXprocess.env.NEXT_PUBLIC_XXX在客户端不存在
  2. SSR不兼容:第三方库在服务端渲染时访问windowdocument
  3. 数据获取错误load函数、getServerSideProps或服务器组件中的API调用失败
  4. Hydration不匹配:服务端渲染的HTML与客户端生成的DOM不一致
  5. 静态资源路径错误:构建后静态资源的路径配置不正确
  6. 模块加载失败:JS分块加载失败或第三方CDN不可用
  7. 依赖版本不兼容:不同依赖之间的版本冲突

最佳实践

根据工具选择合适的方案

  • agent-browser用户:使用官方的--init-script参数或addinitscript命令
  • Playwright用户:优先使用原生的page.on('pageerror')context.on('console'),只有在确认有遗漏时才添加addInitScript()
  • Puppeteer用户:使用page.evaluateOnNewDocument()
  • Selenium用户:使用DevTools API + Runtime.enable

结合CDP协议作为终极保障

对于最复杂的情况,可以直接使用Chrome DevTools Protocol的Runtime.enable方法,这能捕获所有可能的错误。

仍然建议在HTML中添加内联脚本作为最后一道防线

虽然现在工具已经提供了解决方案,但在HTML最顶部添加内联错误捕获脚本仍然是最保险的做法,适用于任何环境。

确认工具版本符合要求

所有主流工具都已经解决了这个问题,特别是Playwright v1.40+和agent-browser 2026年4月后的版本,原生错误捕获能力有了质的飞跃。

添加框架级别的全局错误处理

作为第二道防线,捕获那些可能漏网的错误。

结语

好消息是:这个曾经困扰无数开发者的"隐形错误"问题,现在已经有了完美的官方解决方案。

  • agent-browser在2026年4月17日的PR #1257中彻底解决了这个问题,添加了完整的预导航初始化脚本支持
  • Playwright不仅早就提供了对应的解决方案,而且在v1.40+版本中大幅改进了原生捕获能力,现在是所有工具中表现最好的
  • PuppeteerSelenium也都有成熟的解决方案
  • 所有解决方案的核心思想都是:在页面加载前注入错误捕获脚本

现在你再也不用担心"页面白屏但控制台为空"的问题了。按照上面的官方方法配置,你将能够捕获所有发生在页面初始化阶段的错误。

如果你也遇到过类似的"隐形错误",欢迎在评论区分享你的经历。

参考文献

[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