前言
你在调试一个前端工程问题,页面表现异常。你让 AI 用浏览器工具帮你打开页面,检查控制台有没有报错。
AI 打开页面,扫了一圈,告诉你:控制台干干净净,没有任何错误。
你将信将疑,自己用 Chrome 打开 DevTools——三条鲜红的报错赫然在列,页面其实已经白屏崩溃了。AI 明明用 Headless 浏览器访问了同一个页面,为什么什么都没抓到?
这不是某个工具的bug,也不是你的配置问题。这是一个藏在Headless浏览器设计深处的时序陷阱,过去几年里让无数开发者怀疑人生。它影响所有现代前端框架——React、Vue、SvelteKit、Next.js,无一幸免。
好消息是,到了2026年,所有主流工具都已经给出了官方解决方案。但在揭开答案之前,先让我们搞清楚一件事:这个错误到底是怎么"消失"的?
问题重现:那个消失的错误
案发现场
一切看起来都正常:
- 本地开发模式运行良好,构建部署也没问题
- 用普通Chrome打开,DevTools控制台清晰地显示着红色错误
- 但用Headless工具访问时(旧版本),诡异的事情发生了:
agent-browser console 输出——空的- Puppeteer的
page.on('pageerror') ——没触发 - Selenium的日志系统——一片空白
页面明明已经白屏崩溃,但这些工具异口同声地告诉你:“没有错误。”
注意:Playwright v1.40+ 已大幅改进此问题,默认会捕获大多数错误。以下分析主要针对旧版本行为。
不是你的错,是工具的时序缺陷
真相出人意料地简单:所有高层封装的Headless浏览器工具,过去都把错误捕获代码放在了页面加载完成之后才注入。
而现代前端框架的错误,恰恰发生在页面加载过程中——在框架初始化、组件挂载、hydration的几百毫秒里。你的错误捕获器还没就位,错误已经发生又消失了。
这是一个被广泛讨论的行业问题,在GitHub上有数千个相关issue。
问题本质:无法逾越的时序陷阱
精确的时间线分析
让我们把整个过程拆解到毫秒级,你就会明白为什么错误曾经永远抓不到:
| 时间点 | 发生的事件 | 旧版工具的错误捕获系统状态 | 新版Playwright状态 |
|---|
| T0 | 浏览器开始请求HTML | 未初始化 | 已初始化 |
| T1 | HTML下载完成,开始解析 | 未初始化 | 已初始化 |
| T2 | 解析到框架入口脚本并开始执行 | 未初始化 | 已初始化 |
| T3 | 框架初始化开始(ReactDOM.render、createApp、kit.start()等) | 未初始化 | 已初始化 |
| T4 | 组件初始化时抛出未处理的Promise拒绝或同步错误 | 未初始化 | 正在捕获 |
| T5 | 浏览器触发DOMContentLoaded事件 | 未初始化 | 正在捕获 |
| T6 | 浏览器触发load事件 | 未初始化 | 正在捕获 |
| T7 | Headless工具检测到页面加载完成 | 未初始化 | 正在捕获 |
| T8 | Headless工具注入错误捕获代码 | 已错过错误 | 已捕获完成 |
| 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参数(推荐)
1
2
| # 从文件加载初始化脚本,在第一次导航前执行
agent-browser open --init-script ./error-capture.js http://localhost:5173
|
方法2:使用batch命令(推荐用于复杂场景)
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:使用环境变量全局设置
1
2
3
| # 对所有agent-browser命令生效,支持多个脚本用逗号分隔
export AGENT_BROWSER_INIT_SCRIPTS="./error-capture.js,./another-script.js"
agent-browser open http://localhost:5173
|
方法4:移除已添加的初始化脚本
1
2
3
4
5
| # 查看所有已添加的初始化脚本及其ID
agent-browser listinitscripts
# 移除指定ID的初始化脚本
agent-browser removeinitscript <identifier>
|
完整的error-capture.js文件
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('========================');
};
|
获取捕获到的错误
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错误,这两个事件已经足够。
最佳实践实现
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()方法。
官方说明
“该函数在以下情况之一被调用:每当页面导航时;每当子框架被附加或导航时。函数在文档创建后但在任何页面脚本运行之前被调用。”
实现代码
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)
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>的最最顶部:
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>
|
各框架的具体放置位置
- SvelteKit:
src/app.html - Next.js:
app/layout.tsx的最顶部,在任何导入之前 - Nuxt.js:
app.vue的<script setup>之前,或使用nuxt.config.ts的app.head.script配置 - React + Vite:
index.html - Vue + Vite:
index.html
各框架的全局错误处理(第二道防线)
作为第二道防线,你应该在每个框架中添加全局错误处理:
SvelteKit
在src/hooks.client.js中添加:
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中添加:
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中添加:
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
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
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模式运行
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 });
|
检查构建输出
很多早期错误在构建阶段就能发现:
1
2
3
4
5
| # 构建项目并检查警告和错误
npm run build
# 预览构建结果(这会暴露很多开发模式下看不到的问题)
npm run preview
|
启用Chrome的详细日志
1
2
| # 启动Chrome时添加这些参数
chrome --headless=new --enable-logging --v=1 --log-level=0 http://localhost:5173
|
常见的早期错误原因
这些是所有框架都会遇到的、最容易在初始化阶段抛出且难以捕获的错误:
- 环境变量未定义:
import.meta.env.VITE_XXX或process.env.NEXT_PUBLIC_XXX在客户端不存在 - SSR不兼容:第三方库在服务端渲染时访问
window或document - 数据获取错误:
load函数、getServerSideProps或服务器组件中的API调用失败 - Hydration不匹配:服务端渲染的HTML与客户端生成的DOM不一致
- 静态资源路径错误:构建后静态资源的路径配置不正确
- 模块加载失败:JS分块加载失败或第三方CDN不可用
- 依赖版本不兼容:不同依赖之间的版本冲突
最佳实践
根据工具选择合适的方案
- 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+版本中大幅改进了原生捕获能力,现在是所有工具中表现最好的
- Puppeteer和Selenium也都有成熟的解决方案
- 所有解决方案的核心思想都是:在页面加载前注入错误捕获脚本
现在你再也不用担心"页面白屏但控制台为空"的问题了。按照上面的官方方法配置,你将能够捕获所有发生在页面初始化阶段的错误。
如果你也遇到过类似的"隐形错误",欢迎在评论区分享你的经历。
参考文献
[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