pin_drop当前位置:知识文库 ❯ 图文

JS集成:HTML onerror 事件 - 详细教程与实战指南

教程简介

onerror 事件是前端错误处理的核心机制,涵盖了多种错误场景:window.onerror 处理全局 JavaScript 运行时错误,元素级的 onerror 处理资源加载错误(如图片、脚本、样式表加载失败),以及 unhandledrejection 事件处理未捕获的 Promise 拒绝。完善的错误处理和上报机制是生产级应用的必备能力。

本教程将深入讲解错误事件类型、错误对象属性、错误捕获与冒泡、资源加载错误、Promise 未处理拒绝、错误上报以及全局错误处理策略。


核心概念

错误事件类型

事件 触发对象 触发时机 用途
window.onerror window JavaScript 运行时错误 全局错误捕获
window.onunhandledrejection window Promise 未处理拒绝 异步错误捕获
element.onerror 元素 资源加载失败 资源错误处理

window.onerror 参数

代码示例

window.onerror = function(message, source, lineno, colno, error) {
    // message: 错误消息
    // source: 错误脚本的 URL
    // lineno: 错误行号
    // colno: 错误列号
    // error: Error 对象(包含堆栈信息)
};

window.addEventListener('error') 与 window.onerror 的区别

特性 window.onerror window.addEventListener('error')
语法 赋值方式 事件监听方式
资源加载错误 不捕获 捕获(event.target 为元素)
返回值 返回 true 阻止默认行为 使用 preventDefault()
多个处理程序 只能绑定一个 可以绑定多个
事件对象 无(使用参数) 有 ErrorEvent 对象

Error 对象属性

属性 说明
message 错误消息
name 错误类型名称
stack 错误堆栈信息
fileName 错误文件名(非标准)
lineNumber 错误行号(非标准)
columnNumber 错误列号(非标准)

错误类型

错误类型 说明 典型场景
SyntaxError 语法错误 代码解析失败
ReferenceError 引用错误 访问未定义的变量
TypeError 类型错误 对 null 调用方法
RangeError 范围错误 递归溢出栈
URIError URI 错误 decodeURI 参数无效
EvalError eval 错误 eval 使用不当

语法与用法

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>

浏览器兼容性

特性 Chrome Firefox Safari Edge IE
window.onerror 全部 全部 全部 全部 6+
onerror 五参数 全部 全部 全部 全部 9+
ErrorEvent 全部 全部 全部 全部 不支持
error.stack 全部 全部 全部 全部 10+
unhandledrejection 49+ 69+ 11+ 15+ 不支持
rejectionhandled 49+ 69+ 11+ 15+ 不支持
navigator.sendBeacon 39+ 31+ 11.1+ 14+ 不支持
资源错误捕获 全部 全部 全部 全部 不支持
crossorigin 属性 全部 全部 全部 全部 11+

注意事项与最佳实践

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. 页面卸载前上报

使用 sendBeaconbeforeunload 事件确保页面关闭前的错误也能上报。

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 事件是前端错误处理的基石,完善的错误处理机制对于生产级应用至关重要。以下是核心要点:

  1. 三种错误类型:运行时错误(onerror)、资源加载错误(addEventListener 捕获阶段)、Promise 拒绝(unhandledrejection)。

  2. 同时监听:必须同时设置三种监听器才能覆盖所有错误场景。

  3. 跨域脚本:添加 crossorigin="anonymous" 属性获取详细错误信息。

  4. 错误上报:使用 sendBeacon、Image 请求或 fetch 上报,确保页面卸载时也能发送。

  5. 错误去重:使用 Set 或哈希对错误进行去重,避免重复上报。

  6. 采样率:高流量应用使用采样率控制上报量。

  7. Source Map:生产环境不暴露 Source Map,在服务端用于还原错误位置。

  8. 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 事件同步上报。

标签: Script标签 JavaScript HTML 事件 onerror CORS

本文由小确幸生活整理发布,转载请注明出处

本文涉及AI创作

内容由AI创作,请仔细甄别

list快速访问

上一篇: JS集成:HTML ondrop 事件详 - 详细教程与实战指南 下一篇: JS集成:HTML 事件冒泡与捕获详解 - 详细教程与实战指南

poll相关推荐