人人都会AI编程

21.3 全局异常捕获:未捕获异常、Promise 异常、渲染进程白屏处理

更新时间:2026-07-11

桌面应用与 Web 应用在错误处理上的最大区别在于:用户无法简单地“刷新页面”。如果一个未被捕获的异常导致窗口白屏或应用闪退,用户只能重启程序,体验极差。因此,Electron 应用必须建立一套全局异常捕获机制,确保在任何意外发生时,程序至少能优雅降级,而不是直接崩溃。

本节将分别覆盖主进程和渲染进程的异常捕获策略,并给出白屏场景的应对方案。

21.3.1 主进程的全局异常处理

主进程是应用的心脏,一旦它崩溃,所有窗口都会消失。Node.js 提供了两个全局事件来兜底未捕获的异常。

未捕获的同步异常

使用 process.on('uncaughtException') 可以捕获所有未被 try/catch 包裹的同步错误。

// main.js(主进程入口)
process.on('uncaughtException', (error) => {
  console.error('主进程未捕获异常:', error);
  // 记录到日志文件
  logToFile(`uncaughtException: ${error.stack}`);
  
  // 友好提示用户(如果还能弹出窗口)
  dialog.showErrorBox('发生错误', '应用遇到了一个意外问题,建议重新启动。');
  
  // 这里的策略通常是:记录日志后,让应用继续运行或优雅退出
  // 如果错误严重,可调用 app.quit()
});

真实建议:不要在此处盲目地重新创建窗口或继续执行,因为错误可能已经污染了主进程的状态。许多成熟应用的做法是记录错误并提示用户重启,或自动重启应用(使用 app.relaunch()app.exit())。

未捕获的 Promise 拒绝

从 Node.js 15 开始,未处理的 Promise 拒绝会触发 unhandledRejection 事件。

process.on('unhandledRejection', (reason, promise) => {
  console.error('主进程未处理的 Promise 拒绝:', reason);
  logToFile(`unhandledRejection: ${reason?.stack || reason}`);
  
  // 根据需要决定是否退出
  // 对于非致命的 API 调用失败,可以只记录日志
});

注意:在生产环境中,不要依赖默认行为(打印警告),必须显式监听这两个事件,否则应用可能在无任何提示的情况下直接退出。

结合日志系统

在主进程的异常处理中,最重要的动作是将错误信息写入本地文件。可以使用 electron-log 等库,或自己写一个简单的追加日志模块。

const fs = require('fs');
const path = require('path');

function logToFile(message) {
  const logPath = path.join(app.getPath('userData'), 'error.log');
  const line = `[${new Date().toISOString()}] ${message}\n`;
  fs.appendFileSync(logPath, line);
}

21.3.2 渲染进程的全局异常处理

渲染进程中,未捕获的异常会导致当前窗口的 JavaScript 执行停止,通常表现为白屏或部分功能失效。通过监听 windowerrorunhandledrejection 事件,并结合 Electron 的 webContents 事件,可以搭建完整的保护网。

捕获渲染进程中的同步错误和 Promise 拒绝

在一个渲染进程的预加载脚本或页面脚本中,可以设置全局守卫:

// preload.js 或直接在渲染进程的 HTML 中执行
window.addEventListener('error', (event) => {
  const error = event.error || event.message;
  console.error('渲染进程错误:', error);
  // 发送错误信息给主进程进行日志记录
  window.electronAPI.sendError({
    type: 'renderer-error',
    message: error?.stack || error,
    url: event.filename,
    line: event.lineno,
  });
  // 阻止默认行为,避免在控制台额外报错
  event.preventDefault();
});

window.addEventListener('unhandledrejection', (event) => {
  console.error('渲染进程未处理的 Promise 拒绝:', event.reason);
  window.electronAPI.sendError({
    type: 'renderer-unhandled-rejection',
    reason: event.reason?.stack || event.reason,
  });
  event.preventDefault();
});

在预加载脚本中通过 contextBridge 暴露 sendError 接口:

// preload.js
const { contextBridge, ipcRenderer } = require('electron');

contextBridge.exposeInMainWorld('electronAPI', {
  sendError: (errorInfo) => ipcRenderer.send('report-error', errorInfo),
});

主进程接收错误并记录:

ipcMain.on('report-error', (event, errorInfo) => {
  logToFile(`Renderer ${errorInfo.type}: ${errorInfo.message || errorInfo.reason}`);
});

利用 webContents 事件监听渲染进程崩溃

除了 JavaScript 层面的异常,渲染进程本身可能因为内存不足等原因崩溃。Electron 提供了专门的事件:

// main.js,创建窗口时
const mainWindow = new BrowserWindow({ /* ... */ });

mainWindow.webContents.on('crashed', (event) => {
  console.error('渲染进程崩溃');
  // 通常做法是重新加载窗口
  mainWindow.loadFile('error.html'); // 或显示一个友好的崩溃界面
});

mainWindow.webContents.on('unresponsive', () => {
  console.warn('渲染进程无响应');
  // 可以考虑强制重载
  mainWindow.webContents.forcefullyCrashRenderer();
});

mainWindow.on('unresponsive', () => {
  // 备用处理
});

21.3.3 白屏处理策略

白屏是用户遇到最令人困惑的故障之一。可能的原因包括:

  • 渲染进程加载 HTML 失败(路径错误、网络问题)。
  • 窗口加载了 about:blank 或空内容。
  • 页面 JavaScript 出错导致整个应用失活。
  • CSS 或 DOM 完全没渲染出来。

监听加载失败事件

在主进程中监听 did-fail-load 事件,可捕获加载阶段的错误:

mainWindow.webContents.on('did-fail-load', (event, errorCode, errorDescription, validatedURL) => {
  console.error('页面加载失败:', errorDescription);
  // 显示内置的错误页面
  mainWindow.loadFile('error.html').catch((err) => {
    // 甚至连错误页都加载失败,则显示纯文本
    mainWindow.webContents.loadURL(`data:text/html,<h1>应用崩溃</h1><p>错误代码: ${errorCode}</p>`);
  });
});

预置兜底错误页

在应用资源目录下放置一个 error.html,该页面不依赖任何外部脚本,仅包含基本的 HTML 和 CSS,用于告知用户重启或联系支持。

<!-- error.html -->
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>错误</title></head>
<body style="font-family: sans-serif; padding: 2rem;">
  <h1>应用遇到了问题</h1>
  <p>请尝试重启应用。如果问题持续,请联系支持团队。</p>
  <button onclick="window.location.reload()">重新加载</button>
</body>
</html>

页面白屏自检脚本

也可以在前端代码中加入一个“心跳”检测,如果一定时间内页面主内容还未渲染,则触发重载。但需谨慎使用,避免正常加载时的误判。

// 渲染进程的入口 JavaScript
let loaded = false;
window.addEventListener('DOMContentLoaded', () => {
  loaded = true;
});

setTimeout(() => {
  if (!loaded) {
    console.error('页面加载超时,可能是白屏');
    window.electronAPI.sendError({
      type: 'white-screen',
      message: '页面加载超时',
    });
    // 由主进程决定是否重载窗口
  }
}, 10000); // 10秒阈值

主进程处理白屏上报:

ipcMain.on('report-error', (event, info) => {
  if (info.type === 'white-screen') {
    // 可以尝试重新加载窗口
    const win = BrowserWindow.fromWebContents(event.sender);
    if (win) {
      win.loadFile('index.html');
    }
  }
});

21.3.4 生产环境建议

  • 日志收集:所有全局异常应不仅记录在本地,还应考虑上报到远程监控系统(如 Sentry、自建日志服务),以便分析崩溃趋势。
  • 用户通知:在非致命错误时,用原生对话框(dialog.showErrorBox)或自定义 UI 提示用户,避免静默失败。
  • 自动恢复:对于渲染进程崩溃,可以尝试自动重载窗口;对于主进程崩溃,大多数方案是让用户手动重启,也可使用 Electron 的 autoUpdater 类似机制触发静默重启。
  • 测试覆盖:全局异常处理器本身应该通过模拟错误来测试其有效性,确保在生产环境中确实能兜底。

全局异常捕获不是“锦上添花”,而是桌面应用质量的底线。做好了它,你的应用才能从“有时候会闪退的网页壳”变成用户信赖的生产力工具。