pin_drop当前位置:知识文库 ❯ 图文
JS集成:HTML onerror 事件 - 详细教程与实战指南
教程简介
onerror 事件是前端错误处理的核心机制,涵盖了多种错误场景:window.onerror 处理全局 JavaScript 运行时错误,元素级的 onerror 处理资源加载错误(如图片、脚本、样式表加载失败),以及 unhandledrejection 事件处理未捕获的 Promise 拒绝。完善的错误处理和上报机制是生产级应用的必备能力。
本教程将深入讲解错误事件类型、错误对象属性、错误捕获与冒泡、资源加载错误、Promise 未处理拒绝、错误上报以及全局错误处理策略。
核心概念
错误事件类型
window.onerror 参数
代码示例
window.onerror = function(message, source, lineno, colno, error) {
// message: 错误消息
// source: 错误脚本的 URL
// lineno: 错误行号
// colno: 错误列号
// error: Error 对象(包含堆栈信息)
};window.addEventListener('error') 与 window.onerror 的区别
Error 对象属性
错误类型
语法与用法
window.onerror
代码示例
window.onerror = function(message, source, lineno, colno, error) {
console.error('全局错误:', message);
console.error('来源:', source);
console.error('位置:', lineno + ':' + colno);
console.error('堆栈:', error ? error.stack : '无');
return true; // 阻止浏览器默认错误显示
};window.addEventListener('error')
代码示例
window.addEventListener('error', function(event) {
// JavaScript 运行时错误
if (event.error) {
console.error('运行时错误:', event.error.message);
console.error('堆栈:', event.error.stack);
}
// 资源加载错误
if (event.target && event.target !== window) {
console.error('资源加载失败:', event.target.src || event.target.href);
}
}, true); // 使用捕获阶段才能捕获资源加载错误元素 onerror
代码示例
<img src="image.jpg" onerror="this.src='fallback.jpg'">
<script src="app.js" onerror="handleScriptError()"></script>
<link rel="stylesheet" href="style.css" onerror="handleStyleError()">unhandledrejection
代码示例
window.addEventListener('unhandledrejection', function(event) {
console.error('未处理的 Promise 拒绝:', event.reason);
event.preventDefault(); // 阻止控制台打印
});代码示例
示例1:全局错误捕获
代码示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>全局错误捕获</title>
<style>
body {
font-family: "Microsoft YaHei", sans-serif;
padding: 20px;
background: #f5f5f5;
}
.error-dashboard {
max-width: 900px;
margin: 0 auto;
}
.trigger-section {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 12px;
margin-bottom: 30px;
}
.trigger-btn {
padding: 12px 16px;
border: none;
border-radius: 8px;
font-size: 14px;
cursor: pointer;
transition: transform 0.2s;
color: white;
}
.trigger-btn:hover { transform: translateY(-2px); }
.btn-syntax { background: #f44336; }
.btn-reference { background: #E91E63; }
.btn-type { background: #9C27B0; }
.btn-range { background: #673AB7; }
.btn-promise { background: #3F51B5; }
.btn-async { background: #2196F3; }
.error-log {
background: white;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0,0,0,0.1);
overflow: hidden;
}
.error-log-header {
padding: 15px 20px;
background: #f44336;
color: white;
display: flex;
justify-content: space-between;
align-items: center;
}
.error-log-header h3 { margin: 0; }
.clear-btn {
padding: 6px 14px;
background: rgba(255,255,255,0.2);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.error-entries {
max-height: 400px;
overflow-y: auto;
padding: 10px;
}
.error-entry {
padding: 12px;
margin-bottom: 8px;
border-radius: 8px;
font-size: 13px;
animation: slideIn 0.3s ease;
}
@keyframes slideIn {
from { opacity: 0; transform: translateX(-10px); }
to { opacity: 1; transform: translateX(0); }
}
.error-entry.runtime {
background: #ffebee;
border-left: 4px solid #f44336;
}
.error-entry.promise {
background: #e3f2fd;
border-left: 4px solid #2196F3;
}
.error-entry.resource {
background: #fff3e0;
border-left: 4px solid #FF9800;
}
.error-type {
font-weight: bold;
margin-bottom: 4px;
}
.error-message { color: #333; }
.error-location { color: #888; font-size: 12px; margin-top: 4px; }
.error-stack {
margin-top: 6px;
padding: 8px;
background: rgba(0,0,0,0.05);
border-radius: 4px;
font-family: monospace;
font-size: 11px;
white-space: pre-wrap;
max-height: 80px;
overflow-y: auto;
color: #666;
}
.stats-bar {
display: flex;
gap: 16px;
margin-bottom: 20px;
}
.stat-item {
padding: 10px 20px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
text-align: center;
}
.stat-value {
font-size: 24px;
font-weight: bold;
}
.stat-label {
font-size: 12px;
color: #888;
}
</style>
</head>
<body>
<div class="error-dashboard">
<h1>全局错误捕获</h1>
<p>点击下方按钮触发不同类型的错误,观察错误捕获效果。</p>
<div class="stats-bar">
<div class="stat-item">
<div class="stat-value" id="totalCount" style="color: #f44336;">0</div>
<div class="stat-label">总错误数</div>
</div>
<div class="stat-item">
<div class="stat-value" id="runtimeCount" style="color: #E91E63;">0</div>
<div class="stat-label">运行时错误</div>
</div>
<div class="stat-item">
<div class="stat-value" id="promiseCount" style="color: #2196F3;">0</div>
<div class="stat-label">Promise 错误</div>
</div>
</div>
<div class="trigger-section">
<button class="trigger-btn btn-reference" onclick="triggerReferenceError()">ReferenceError</button>
<button class="trigger-btn btn-type" onclick="triggerTypeError()">TypeError</button>
<button class="trigger-btn btn-range" onclick="triggerRangeError()">RangeError</button>
<button class="trigger-btn btn-promise" onclick="triggerPromiseError()">Promise 拒绝</button>
<button class="trigger-btn btn-async" onclick="triggerAsyncError()">Async 错误</button>
</div>
<div class="error-log">
<div class="error-log-header">
<h3>错误日志</h3>
<button class="clear-btn" onclick="clearErrors()">清空</button>
</div>
<div class="error-entries" id="errorEntries"></div>
</div>
</div>
<script>
let totalErrors = 0;
let runtimeErrors = 0;
let promiseErrors = 0;
// 全局错误捕获 - onerror
window.onerror = function(message, source, lineno, colno, error) {
totalErrors++;
runtimeErrors++;
updateStats();
addErrorEntry({
type: '运行时错误',
category: 'runtime',
message: message,
location: `${source}:${lineno}:${colno}`,
stack: error ? error.stack : '无堆栈信息'
});
return true; // 阻止浏览器默认错误显示
};
// 全局错误捕获 - addEventListener(捕获资源加载错误)
window.addEventListener('error', function(event) {
if (event.target && event.target !== window) {
const tag = event.target.tagName;
const src = event.target.src || event.target.href || '未知';
totalErrors++;
updateStats();
addErrorEntry({
type: '资源加载错误',
category: 'resource',
message: `${tag} 加载失败: ${src}`,
location: src,
stack: '资源加载错误无堆栈'
});
}
}, true); // 捕获阶段
// Promise 未处理拒绝
window.addEventListener('unhandledrejection', function(event) {
totalErrors++;
promiseErrors++;
updateStats();
addErrorEntry({
type: '未处理的 Promise 拒绝',
category: 'promise',
message: event.reason ? (event.reason.message || String(event.reason)) : '未知原因',
location: '-',
stack: event.reason && event.reason.stack ? event.reason.stack : '无堆栈信息'
});
event.preventDefault();
});
// 触发错误的函数
function triggerReferenceError() {
// 访问未定义的变量
console.log(undefinedVariable);
}
function triggerTypeError() {
// 对 null 调用方法
const obj = null;
obj.method();
}
function triggerRangeError() {
// 递归溢出
function recurse() { recurse(); }
recurse();
}
function triggerPromiseError() {
// 未处理的 Promise 拒绝
Promise.reject(new Error('手动触发的 Promise 拒绝'));
}
async function triggerAsyncError() {
// async 函数中的未捕获错误
const response = await fetch('nonexistent-api-endpoint-12345');
}
function updateStats() {
document.getElementById('totalCount').textContent = totalErrors;
document.getElementById('runtimeCount').textContent = runtimeErrors;
document.getElementById('promiseCount').textContent = promiseErrors;
}
function addErrorEntry(error) {
const entries = document.getElementById('errorEntries');
const entry = document.createElement('div');
entry.className = 'error-entry ' + error.category;
entry.innerHTML = `
<div class="error-type">${error.type}</div>
<div class="error-message">${error.message}</div>
<div class="error-location">${error.location}</div>
<div class="error-stack">${error.stack}</div>
`;
entries.insertBefore(entry, entries.firstChild);
}
function clearErrors() {
document.getElementById('errorEntries').innerHTML = '';
totalErrors = 0;
runtimeErrors = 0;
promiseErrors = 0;
updateStats();
}
</script>
</body>
</html>示例2:资源加载错误处理
代码示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>资源加载错误处理</title>
<style>
body {
font-family: "Microsoft YaHei", sans-serif;
padding: 20px;
background: #f5f5f5;
}
.demo-section {
margin-bottom: 30px;
padding: 20px;
background: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.demo-section h3 { margin-top: 0; }
.image-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 16px;
}
.image-card {
border-radius: 8px;
overflow: hidden;
background: #f5f5f5;
position: relative;
}
.image-card img {
width: 100%;
height: 150px;
object-fit: cover;
display: block;
}
.image-card .fallback {
width: 100%;
height: 150px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: #e0e0e0;
color: #888;
font-size: 13px;
}
.image-card .fallback .icon { font-size: 30px; margin-bottom: 8px; }
.image-card .label {
padding: 8px;
font-size: 12px;
color: #666;
text-align: center;
}
.status-badge {
display: inline-block;
padding: 3px 10px;
border-radius: 12px;
font-size: 12px;
font-weight: bold;
}
.status-loaded { background: #e8f5e9; color: #2e7d32; }
.status-failed { background: #ffebee; color: #c62828; }
.status-retrying { background: #fff3e0; color: #e65100; }
</style>
</head>
<body>
<h1>资源加载错误处理</h1>
<div class="demo-section">
<h3>图片加载错误与回退</h3>
<div class="image-grid" id="imageGrid"></div>
</div>
<script>
const images = [
{ src: 'https://picsum.photos/200/150?random=1', label: '正常图片1' },
{ src: 'https://nonexistent-domain-12345.com/image.jpg', label: '错误域名' },
{ src: 'https://picsum.photos/200/150?random=2', label: '正常图片2' },
{ src: 'https://httpbin.org/status/404', label: '404 错误' },
{ src: 'https://picsum.photos/200/150?random=3', label: '正常图片3' },
{ src: 'https://example.invalid/image.png', label: '无效域名' },
];
const FALLBACK_IMAGE = 'data:image/svg+xml,' + encodeURIComponent(
'<svg xmlns="http://www.w3.org/2000/svg" width="200" height="150" fill="%23e0e0e0">' +
'<rect width="200" height="150"/>' +
'<text x="100" y="75" text-anchor="middle" fill="%23999" font-size="14">图片加载失败</text></svg>'
);
const MAX_RETRIES = 2;
// 全局资源错误捕获
window.addEventListener('error', function(event) {
const target = event.target;
if (target.tagName === 'IMG') {
handleImageError(target);
}
}, true);
function handleImageError(img) {
const retryCount = parseInt(img.dataset.retry || '0');
if (retryCount < MAX_RETRIES) {
// 重试
img.dataset.retry = (retryCount + 1).toString();
updateStatus(img, 'retrying');
setTimeout(() => {
img.src = img.dataset.originalSrc + '?retry=' + (retryCount + 1);
}, 500 * (retryCount + 1));
} else {
// 使用回退图片
img.src = FALLBACK_IMAGE;
updateStatus(img, 'failed');
}
}
function updateStatus(img, status) {
const card = img.closest('.image-card');
if (!card) return;
const badge = card.querySelector('.status-badge');
if (badge) {
if (status === 'retrying') {
badge.className = 'status-badge status-retrying';
badge.textContent = '重试中...';
} else if (status === 'failed') {
badge.className = 'status-badge status-failed';
badge.textContent = '加载失败';
}
}
}
// 渲染图片
const grid = document.getElementById('imageGrid');
images.forEach(img => {
const card = document.createElement('div');
card.className = 'image-card';
card.innerHTML = `
<img src="${img.src}" data-original-src="${img.src}" alt="${img.label}"
onload="this.closest('.image-card').querySelector('.status-badge').className='status-badge status-loaded'; this.closest('.image-card').querySelector('.status-badge').textContent='已加载';">
<div class="label">
${img.label} <span class="status-badge status-retrying">加载中...</span>
</div>
`;
grid.appendChild(card);
});
</script>
</body>
</html>示例3:Promise 未处理拒绝
代码示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Promise 未处理拒绝</title>
<style>
body {
font-family: "Microsoft YaHei", sans-serif;
padding: 20px;
background: #f5f5f5;
}
.container { max-width: 800px; margin: 0 auto; }
.demo-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-bottom: 20px;
}
.demo-card {
padding: 20px;
background: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.demo-card h4 { margin-top: 0; }
.demo-card p { font-size: 13px; color: #666; line-height: 1.6; }
.trigger-btn {
padding: 10px 20px;
border: none;
border-radius: 6px;
color: white;
cursor: pointer;
font-size: 14px;
margin-top: 10px;
}
.btn-danger { background: #f44336; }
.btn-warning { background: #FF9800; }
.btn-info { background: #2196F3; }
.btn-success { background: #4CAF50; }
.rejection-log {
padding: 20px;
background: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.rejection-log h3 { margin-top: 0; }
.log-entry {
padding: 10px;
margin-bottom: 8px;
border-radius: 8px;
font-size: 13px;
border-left: 4px solid;
}
.log-entry.unhandled { background: #ffebee; border-color: #f44336; }
.log-entry.handled { background: #e8f5e9; border-color: #4CAF50; }
.log-entry .reason { font-weight: bold; }
.log-entry .stack { font-family: monospace; font-size: 11px; color: #888; margin-top: 4px; }
</style>
</head>
<body>
<div class="container">
<h1>Promise 未处理拒绝</h1>
<div class="demo-grid">
<div class="demo-card">
<h4>未处理的 Promise 拒绝</h4>
<p>Promise 被拒绝但没有 .catch() 处理,会触发 unhandledrejection 事件。</p>
<button class="trigger-btn btn-danger" onclick="unhandledRejection()">触发未处理拒绝</button>
</div>
<div class="demo-card">
<h4>已处理的 Promise 拒绝</h4>
<p>Promise 被拒绝但有 .catch() 处理,不会触发 unhandledrejection 事件。</p>
<button class="trigger-btn btn-success" onclick="handledRejection()">触发已处理拒绝</button>
</div>
<div class="demo-card">
<h4>Async 函数未捕获错误</h4>
<p>async 函数中抛出错误但未 try-catch,会触发 unhandledrejection 事件。</p>
<button class="trigger-btn btn-warning" onclick="asyncUnhandledError()">触发 Async 错误</button>
</div>
<div class="demo-card">
<h4>Async 函数已捕获错误</h4>
<p>async 函数中使用 try-catch 捕获错误,不会触发 unhandledrejection 事件。</p>
<button class="trigger-btn btn-info" onclick="asyncHandledError()">触发已捕获 Async 错误</button>
</div>
</div>
<div class="rejection-log">
<h3>拒绝事件日志</h3>
<div id="rejectionLog"></div>
</div>
</div>
<script>
// 监听未处理的 Promise 拒绝
window.addEventListener('unhandledrejection', function(event) {
addLog('unhandled', event.reason);
event.preventDefault(); // 阻止控制台打印
});
// 监听已处理的拒绝(rejectionhandled)
window.addEventListener('rejectionhandled', function(event) {
console.log('拒绝已被处理:', event.reason);
});
function unhandledRejection() {
Promise.reject(new Error('这是一个未处理的 Promise 拒绝'));
}
function handledRejection() {
Promise.reject(new Error('这是一个已处理的 Promise 拒绝'))
.catch(err => {
addLog('handled', err);
});
}
async function asyncUnhandledError() {
const result = await fetch('https://nonexistent-api.example.com/data');
}
async function asyncHandledError() {
try {
const result = await fetch('https://nonexistent-api.example.com/data');
} catch (err) {
addLog('handled', err);
}
}
function addLog(type, reason) {
const log = document.getElementById('rejectionLog');
const entry = document.createElement('div');
entry.className = 'log-entry ' + type;
const message = reason instanceof Error ? reason.message : String(reason);
const stack = reason instanceof Error ? reason.stack : '';
entry.innerHTML = `
<span class="reason">[${type === 'unhandled' ? '未处理' : '已处理'}] ${message}</span>
${stack ? '<div class="stack">' + stack.split('\n').slice(0, 3).join('\n') + '</div>' : ''}
`;
log.insertBefore(entry, log.firstChild);
}
</script>
</body>
</html>示例4:错误上报系统
代码示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>错误上报系统</title>
<style>
body {
font-family: "Microsoft YaHei", sans-serif;
padding: 20px;
background: #1a1a2e;
color: white;
}
.app { max-width: 900px; margin: 0 auto; }
.controls {
display: flex;
gap: 10px;
flex-wrap: wrap;
margin-bottom: 20px;
}
.ctrl-btn {
padding: 10px 20px;
border: none;
border-radius: 8px;
color: white;
cursor: pointer;
font-size: 14px;
}
.ctrl-btn.red { background: #f44336; }
.ctrl-btn.blue { background: #2196F3; }
.ctrl-btn.green { background: #4CAF50; }
.ctrl-btn.orange { background: #FF9800; }
.report-panel {
background: #16213e;
border-radius: 12px;
overflow: hidden;
}
.panel-header {
padding: 15px 20px;
background: #1e2a4a;
display: flex;
justify-content: space-between;
align-items: center;
}
.panel-header h3 { margin: 0; }
.report-count {
padding: 4px 12px;
background: #f44336;
border-radius: 12px;
font-size: 13px;
}
.report-list {
max-height: 500px;
overflow-y: auto;
padding: 10px;
}
.report-item {
padding: 12px;
margin-bottom: 8px;
background: #1e2a4a;
border-radius: 8px;
font-size: 13px;
}
.report-item .header {
display: flex;
justify-content: space-between;
margin-bottom: 6px;
}
.report-item .error-type {
font-weight: bold;
color: #f44336;
}
.report-item .timestamp {
color: #888;
font-size: 11px;
}
.report-item .message { color: #ddd; }
.report-item .meta {
margin-top: 6px;
font-size: 11px;
color: #888;
}
.report-item .stack {
margin-top: 6px;
padding: 8px;
background: rgba(0,0,0,0.2);
border-radius: 4px;
font-family: monospace;
font-size: 11px;
white-space: pre-wrap;
max-height: 60px;
overflow-y: auto;
color: #aaa;
}
.send-btn {
padding: 8px 16px;
background: #4CAF50;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
}
.send-btn:hover { background: #388E3C; }
</style>
</head>
<body>
<div class="app">
<h1>错误上报系统</h1>
<p>点击按钮触发错误,错误将被收集并可以批量上报。</p>
<div class="controls">
<button class="ctrl-btn red" onclick="throw new ReferenceError('变量未定义')">ReferenceError</button>
<button class="ctrl-btn blue" onclick="throw new TypeError('无法读取属性')">TypeError</button>
<button class="ctrl-btn orange" onclick="Promise.reject(new Error('Promise失败'))">Promise 拒绝</button>
<button class="ctrl-btn green" onclick="sendReports()">上报错误</button>
</div>
<div class="report-panel">
<div class="panel-header">
<h3>错误队列</h3>
<span class="report-count" id="reportCount">0</span>
</div>
<div class="report-list" id="reportList"></div>
</div>
</div>
<script>
const errorQueue = [];
const MAX_QUEUE_SIZE = 50;
// 全局错误捕获
window.onerror = function(message, source, lineno, colno, error) {
addErrorToQueue({
type: error ? error.name : 'Error',
message: message,
source: source,
line: lineno,
column: colno,
stack: error ? error.stack : '',
timestamp: Date.now(),
userAgent: navigator.userAgent,
url: window.location.href
});
return true;
};
window.addEventListener('unhandledrejection', function(event) {
const reason = event.reason;
addErrorToQueue({
type: reason instanceof Error ? reason.name : 'UnhandledRejection',
message: reason instanceof Error ? reason.message : String(reason),
source: '',
line: 0,
column: 0,
stack: reason instanceof Error ? reason.stack : '',
timestamp: Date.now(),
userAgent: navigator.userAgent,
url: window.location.href
});
event.preventDefault();
});
function addErrorToQueue(errorData) {
if (errorQueue.length >= MAX_QUEUE_SIZE) {
errorQueue.shift(); // 移除最旧的
}
errorQueue.push(errorData);
renderReportList();
}
function renderReportList() {
const list = document.getElementById('reportList');
const count = document.getElementById('reportCount');
count.textContent = errorQueue.length;
list.innerHTML = '';
errorQueue.slice().reverse().forEach(error => {
const item = document.createElement('div');
item.className = 'report-item';
const time = new Date(error.timestamp).toLocaleTimeString('zh-CN', { hour12: false });
item.innerHTML = `
<div class="header">
<span class="error-type">${error.type}</span>
<span class="timestamp">${time}</span>
</div>
<div class="message">${error.message}</div>
<div class="meta">${error.source}:${error.line}:${error.column} | ${error.url}</div>
${error.stack ? '<div class="stack">' + error.stack.split('\n').slice(0, 4).join('\n') + '</div>' : ''}
`;
list.appendChild(item);
});
}
function sendReports() {
if (errorQueue.length === 0) {
alert('没有待上报的错误');
return;
}
const reports = [...errorQueue];
// 模拟上报(实际项目中发送到服务器)
console.log('上报错误数据:', JSON.stringify(reports, null, 2));
// 使用 navigator.sendBeacon(页面卸载时也能发送)
// navigator.sendBeacon('/api/errors', JSON.stringify(reports));
// 使用 Image 方式上报(兼容性最好)
// new Image().src = '/api/errors?data=' + encodeURIComponent(JSON.stringify(reports));
alert(`已上报 ${reports.length} 条错误(模拟)`);
errorQueue.length = 0;
renderReportList();
}
</script>
</body>
</html>示例5:全局错误处理最佳实践
代码示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>全局错误处理最佳实践</title>
<style>
body {
font-family: "Microsoft YaHei", sans-serif;
padding: 20px;
background: #f5f5f5;
}
.container { max-width: 800px; margin: 0 auto; }
.best-practice {
padding: 20px;
background: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.best-practice h3 { margin-top: 0; color: #333; }
.best-practice p { font-size: 14px; color: #555; line-height: 1.6; }
.code-block {
padding: 15px;
background: #263238;
color: #aed581;
border-radius: 8px;
font-family: monospace;
font-size: 13px;
overflow-x: auto;
white-space: pre;
line-height: 1.6;
}
.tip {
padding: 12px 16px;
border-radius: 8px;
font-size: 13px;
margin-top: 10px;
}
.tip-warn { background: #fff3e0; border-left: 4px solid #FF9800; }
.tip-info { background: #e3f2fd; border-left: 4px solid #2196F3; }
.tip-success { background: #e8f5e9; border-left: 4px solid #4CAF50; }
</style>
</head>
<body>
<div class="container">
<h1>全局错误处理最佳实践</h1>
<div class="best-practice">
<h3>1. 同时使用 onerror 和 addEventListener</h3>
<p>onerror 捕获运行时错误,addEventListener 在捕获阶段捕获资源加载错误。</p>
<div class="code-block">// 运行时错误
window.onerror = function(msg, src, line, col, err) {
reportError({ type: 'runtime', message: msg, source: src, line, col, stack: err?.stack });
return true;
};
// 资源加载错误(必须使用捕获阶段)
window.addEventListener('error', function(e) {
if (e.target !== window) {
reportError({ type: 'resource', tag: e.target.tagName, src: e.target.src || e.target.href });
}
}, true);
// Promise 拒绝
window.addEventListener('unhandledrejection', function(e) {
reportError({ type: 'promise', reason: e.reason?.message || String(e.reason) });
e.preventDefault();
});</div>
</div>
<div class="best-practice">
<h3>2. 错误上报策略</h3>
<p>使用多种上报方式确保可靠性。</p>
<div class="code-block">function reportError(data) {
data.timestamp = Date.now();
data.url = location.href;
data.userAgent = navigator.userAgent;
// 方式1: sendBeacon(推荐,页面卸载时也能发送)
if (navigator.sendBeacon) {
navigator.sendBeacon('/api/errors', JSON.stringify(data));
return;
}
// 方式2: Image 请求(兼容性最好)
new Image().src = '/api/errors?d=' + encodeURIComponent(JSON.stringify(data));
// 方式3: fetch(不保证页面卸载时发送)
// fetch('/api/errors', { method: 'POST', body: JSON.stringify(data) });
}</div>
<div class="tip tip-warn">注意:sendBeacon 有数据大小限制(通常 64KB),大量错误数据应分批发送。</div>
</div>
<div class="best-practice">
<h3>3. 错误采样与聚合</h3>
<p>避免同一错误重复上报,使用采样率控制上报量。</p>
<div class="code-block">class ErrorReporter {
constructor(sampleRate = 1.0) {
this.sampleRate = sampleRate;
this.errorSet = new Set(); // 去重
this.queue = [];
this.flushInterval = 5000; // 5秒批量上报
this.startFlushTimer();
}
report(errorData) {
// 采样
if (Math.random() > this.sampleRate) return;
// 去重(基于错误消息和位置)
const key = `${errorData.message}:${errorData.line}:${errorData.col}`;
if (this.errorSet.has(key)) return;
this.errorSet.add(key);
this.queue.push(errorData);
}
startFlushTimer() {
setInterval(() => this.flush(), this.flushInterval);
}
flush() {
if (this.queue.length === 0) return;
const data = this.queue.splice(0);
navigator.sendBeacon('/api/errors', JSON.stringify(data));
}
}</div>
</div>
<div class="best-practice">
<h3>4. Source Map 支持</h3>
<p>生产环境代码经过压缩混淆,需要 Source Map 还原原始位置。</p>
<div class="tip tip-info">Source Map 文件不应部署到生产服务器,只在错误分析服务端使用。确保 .map 文件不对外暴露。</div>
</div>
<div class="best-practice">
<h3>5. 跨域脚本的错误信息</h3>
<p>跨域脚本默认只返回 "Script error.",无法获取详细错误信息。</p>
<div class="code-block"><!-- 跨域脚本需要添加 crossorigin 属性 -->
<script src="https://cdn.example.com/app.js" crossorigin="anonymous"></script>
<!-- 服务器需要返回正确的 CORS 头 -->
<!-- Access-Control-Allow-Origin: * --></div>
<div class="tip tip-success">添加 crossorigin="anonymous" 属性后,浏览器会发送 CORS 请求获取详细的错误信息。</div>
</div>
</div>
<script>
// 实际的全局错误处理实现
(function() {
const errors = [];
window.onerror = function(message, source, lineno, colno, error) {
errors.push({
type: 'runtime',
message: String(message),
source: source,
line: lineno,
column: colno,
stack: error ? error.stack : null,
time: new Date().toISOString()
});
return true;
};
window.addEventListener('error', function(event) {
if (event.target !== window) {
errors.push({
type: 'resource',
tagName: event.target.tagName,
src: event.target.src || event.target.href,
time: new Date().toISOString()
});
}
}, true);
window.addEventListener('unhandledrejection', function(event) {
errors.push({
type: 'promise',
reason: event.reason instanceof Error
? event.reason.message
: String(event.reason),
stack: event.reason instanceof Error
? event.reason.stack
: null,
time: new Date().toISOString()
});
event.preventDefault();
});
// 页面卸载前上报
window.addEventListener('beforeunload', function() {
if (errors.length > 0 && navigator.sendBeacon) {
navigator.sendBeacon('/api/errors', JSON.stringify(errors));
}
});
})();
</script>
</body>
</html>浏览器兼容性
注意事项与最佳实践
1. 同时监听三种错误类型
代码示例
// 运行时错误
window.onerror = handler;
// 资源加载错误(捕获阶段)
window.addEventListener('error', resourceHandler, true);
// Promise 拒绝
window.addEventListener('unhandledrejection', promiseHandler);2. 跨域脚本需要 crossorigin 属性
代码示例
<script src="https://cdn.example.com/app.js" crossorigin="anonymous"></script>3. 不要吞掉所有错误
window.onerror 返回 true 会阻止浏览器默认的错误显示,在开发环境中应保留控制台输出。
4. 错误去重
同一错误可能被多次触发,使用 Set 或哈希进行去重。
5. 页面卸载前上报
使用 sendBeacon 或 beforeunload 事件确保页面关闭前的错误也能上报。
6. Source Map 安全
Source Map 文件不应暴露在生产环境中,只在服务端用于还原错误位置。
代码规范示例
规范的全局错误处理器
代码示例
class GlobalErrorHandler {
constructor(options = {}) {
this.sampleRate = options.sampleRate || 1.0;
this.maxQueueSize = options.maxQueueSize || 50;
this.reportUrl = options.reportUrl || '/api/errors';
this.queue = [];
this.errorSet = new Set();
this.flushInterval = options.flushInterval || 5000;
this._init();
}
_init() {
// 运行时错误
window.onerror = (msg, src, line, col, err) => {
this._addError({
type: 'runtime',
message: String(msg),
source: src,
line: line,
column: col,
stack: err ? err.stack : null
});
return true;
};
// 资源加载错误
window.addEventListener('error', (e) => {
if (e.target !== window) {
this._addError({
type: 'resource',
tagName: e.target.tagName,
src: e.target.src || e.target.href
});
}
}, true);
// Promise 拒绝
window.addEventListener('unhandledrejection', (e) => {
this._addError({
type: 'promise',
reason: e.reason instanceof Error ? e.reason.message : String(e.reason),
stack: e.reason instanceof Error ? e.reason.stack : null
});
e.preventDefault();
});
// 定时上报
setInterval(() => this.flush(), this.flushInterval);
// 页面卸载前上报
window.addEventListener('beforeunload', () => this.flush());
}
_addError(error) {
if (Math.random() > this.sampleRate) return;
const key = `${error.type}:${error.message}:${error.line}:${error.column}`;
if (this.errorSet.has(key)) return;
this.errorSet.add(key);
error.timestamp = Date.now();
error.url = location.href;
error.userAgent = navigator.userAgent;
if (this.queue.length >= this.maxQueueSize) {
this.queue.shift();
}
this.queue.push(error);
}
flush() {
if (this.queue.length === 0) return;
const data = this.queue.splice(0);
if (navigator.sendBeacon) {
navigator.sendBeacon(this.reportUrl, JSON.stringify(data));
} else {
new Image().src = `${this.reportUrl}?d=${encodeURIComponent(JSON.stringify(data))}`;
}
}
}常见问题与解决方案
问题1:跨域脚本只返回 "Script error."
原因:浏览器出于安全考虑,不向不同源的脚本暴露详细错误信息。
解决方案:添加 crossorigin="anonymous" 属性,服务器返回正确的 CORS 头。
问题2:Promise 错误没有堆栈信息
原因:非 Error 对象的拒绝原因没有 stack 属性。
解决方案:始终使用 new Error() 作为拒绝原因。
代码示例
// 推荐
Promise.reject(new Error('操作失败'));
// 不推荐
Promise.reject('操作失败');问题3:资源加载错误无法通过 onerror 捕获
原因:window.onerror 不捕获资源加载错误。
解决方案:使用 window.addEventListener('error', handler, true) 在捕获阶段监听。
问题4:压缩代码的错误行号无意义
原因:生产环境代码经过压缩混淆,行号指向压缩后的代码。
解决方案:使用 Source Map 在服务端还原原始位置。
问题5:错误上报丢失页面卸载数据
原因:页面卸载时异步请求可能被取消。
解决方案:使用 navigator.sendBeacon() 或 beforeunload 事件同步上报。
总结
onerror 事件是前端错误处理的基石,完善的错误处理机制对于生产级应用至关重要。以下是核心要点:
三种错误类型:运行时错误(onerror)、资源加载错误(addEventListener 捕获阶段)、Promise 拒绝(unhandledrejection)。
同时监听:必须同时设置三种监听器才能覆盖所有错误场景。
跨域脚本:添加
crossorigin="anonymous"属性获取详细错误信息。错误上报:使用 sendBeacon、Image 请求或 fetch 上报,确保页面卸载时也能发送。
错误去重:使用 Set 或哈希对错误进行去重,避免重复上报。
采样率:高流量应用使用采样率控制上报量。
Source Map:生产环境不暴露 Source Map,在服务端用于还原错误位置。
Promise 规范:始终使用
new Error()作为拒绝原因,确保有堆栈信息。
建立完善的全局错误处理和上报机制,能够帮助开发者快速定位和修复线上问题,提升应用的稳定性和用户体验。
常见问题
问题1:跨域脚本只返回 "Script error."?
浏览器出于安全考虑,不向不同源的脚本暴露详细错误信息。 添加 crossorigin="anonymous" 属性,服务器返回正确的 CORS 头。
问题2:Promise 错误没有堆栈信息?
非 Error 对象的拒绝原因没有 stack 属性。 始终使用 new Error() 作为拒绝原因。
问题3:资源加载错误无法通过 onerror 捕获?
window.onerror 不捕获资源加载错误。 使用 window.addEventListener('error', handler, true) 在捕获阶段监听。
问题4:压缩代码的错误行号无意义?
生产环境代码经过压缩混淆,行号指向压缩后的代码。 使用 Source Map 在服务端还原原始位置。
问题5:错误上报丢失页面卸载数据?
页面卸载时异步请求可能被取消。 使用 navigator.sendBeacon() 或 beforeunload 事件同步上报。
本文由小确幸生活整理发布,转载请注明出处
本文涉及AI创作
内容由AI创作,请仔细甄别