一、异常的定义与分类

异常,说白了就是程序运行过程中出现的意外情况。我做了这么多年开发,见过太多因为异常处理不当导致线上事故的案例。嗯,咱们先把这个基础概念理清楚。

1.1 什么是异常

异常是程序执行过程中发生的非正常事件。它会打断正常的指令流。我个人习惯把异常理解为「程序生病了」——有些病能自己扛过去(可恢复),有些病必须送医院(不可恢复)。

核心定义:异常 = 程序执行时遇到的、无法在当前上下文继续执行的问题。

1.2 异常的分类

我一般把异常分成三大类。你想想看,这样分在实际开发中特别好用:

分类 特点 典型例子 处理方式
系统异常 底层系统抛出,不可控 内存溢出、栈溢出 记录日志+重启
业务异常 业务逻辑违反约束 参数校验失败、权限不足 返回错误码+提示用户
运行时异常 代码缺陷导致 空指针、数组越界 修复代码+增加防御

我在项目中遇到过最坑的一次,就是系统异常和业务异常混在一起处理。结果线上出了内存泄漏,业务层还在拼命重试,最后整个集群都挂了。所以分类一定要清晰。

二、同步异常与异步异常

这个区分特别重要。很多新手搞混了,导致异常捕获不到。我刚开始做混合开发时也踩过这个坑。

2.1 同步异常

同步异常发生在代码的同步执行过程中。说白了就是「当场就炸」——你写了个空指针,程序跑到那行直接就崩了。

// 同步异常示例
try {
    let result = JSON.parse(invalidJsonString);
    // 如果 invalidJsonString 格式不对,这里直接抛异常
} catch (error) {
    console.error('JSON解析失败:', error.message);
    // 同步异常可以被 try-catch 捕获
}

同步异常的特点:

  • 可以被 try-catch 直接捕获
  • 异常发生时,当前执行栈立即中断
  • 错误信息包含完整的调用栈

2.2 异步异常

异步异常就麻烦多了。它发生在回调、Promise、事件循环里。你想想看,代码已经执行过去了,异常才冒出来,try-catch 根本抓不到。

// 异步异常示例 - 错误的写法
try {
    setTimeout(() => {
        throw new Error('异步错误');
    }, 1000);
} catch (error) {
    // 这里永远捕获不到!
    console.log('捕获到异常:', error);
}

// 正确的写法
setTimeout(() => {
    try {
        throw new Error('异步错误');
    } catch (error) {
        console.error('在回调内部捕获:', error);
    }
}, 1000);

避坑指南:我曾经在项目中用 try-catch 包裹了整个异步函数,以为能捕获所有异常。结果线上跑了三天,日志里一条异常都没有——其实异常全丢了。后来排查才发现,Promise 里的 reject 必须用 .catch() 处理。

2.3 异步异常的处理模式

我个人习惯用三种模式处理异步异常:

  1. 回调模式:错误优先回调(error-first callback),Node.js 的经典做法
  2. Promise 链:用 .catch() 统一处理
  3. async/await:用 try-catch 包裹,但要注意 await 后面的异常才能捕获
// Promise 链处理
fetch('/api/data')
    .then(response => response.json())
    .catch(error => {
        // 统一处理网络异常
        console.error('请求失败:', error);
    });

// async/await 处理
async function loadData() {
    try {
        const response = await fetch('/api/data');
        const data = await response.json();
        return data;
    } catch (error) {
        // 这里能捕获到异步异常
        console.error('加载数据失败:', error);
        throw error; // 或者返回默认值
    }
}

三、混合开发中的异常来源分析

混合开发(Hybrid App)的异常来源特别复杂。我做过几个大型混合项目,总结下来主要有四个来源。

3.1 原生层异常

原生代码(Java/Kotlin/OC/Swift)抛出的异常。比如:

  • 内存不足导致 Native 崩溃
  • JNI 调用参数类型不匹配
  • 原生组件渲染失败

这类异常最难排查,因为错误信息往往只有个信号量(SIGSEGV、SIGABRT)。

3.2 Web 层异常

H5 页面里的 JavaScript 异常。包括:

  • 语法错误、类型错误
  • DOM 操作异常
  • 网络请求失败
  • 第三方 SDK 报错

3.3 桥接层异常

这是混合开发特有的异常来源。JS 和 Native 通信时最容易出问题:

// 桥接异常示例
// Native 端注册的方法
@JavascriptInterface
public void shareImage(String base64Data) {
    // 如果 base64Data 过大,这里会 OOM
    byte[] data = Base64.decode(base64Data);
    // ...
}

// JS 端调用
window.NativeBridge.shareImage(largeBase64String);
// 如果图片太大,Native 端直接崩溃,JS 端完全不知道

我的经验:桥接层异常有个特点——异常发生在哪一端,错误信息就留在哪一端。所以一定要在两端都加日志。我曾经排查过一个闪退问题,查了三天才发现是 JS 传了个超长字符串给 Native,Native 端解码时内存溢出了。

3.4 环境差异异常

不同手机、不同系统版本、不同 WebView 内核导致的差异:

  • Android 系统 WebView 版本不一致
  • iOS WKWebView 和 UIWebView 行为差异
  • 低端机性能不足导致超时
  • 系统字体、屏幕尺寸适配问题

四、异常来源全景图

下面这张图是我自己整理的混合开发异常来源结构。每次做项目我都会拿它来对照排查:

混合开发异常来源全景图 混合应用 (Hybrid App) 原生层 (Native) 桥接层 (Bridge) Web 层 (H5) 原生异常 内存溢出 / 信号崩溃 JNI 调用异常 原生组件渲染失败 桥接异常 参数序列化失败 通信超时 / 数据截断 两端版本不匹配 Web 异常 JS 语法/运行时错误 网络请求失败 DOM 操作异常 环境差异异常 系统版本差异 | WebView 内核差异 | 设备性能差异 | 屏幕适配问题 统一异常采集与处理平台

五、异常处理的基本原则

讲了这么多,最后总结几条我这些年摸爬滚打总结出来的原则:

  1. 不要吞异常:catch 了什么都不做,等于掩耳盗铃。至少打个日志。
  2. 区分异常类型:业务异常要提示用户,系统异常要记录完整上下文。
  3. 异步异常要特殊处理:别指望 try-catch 能包打天下。
  4. 桥接层加超时:Native 和 JS 通信一定要设超时,防止死等。
  5. 日志要带上下文:光记个「出错了」等于没记。要记设备信息、用户操作路径、参数值。

一个小技巧:我习惯在项目初期就搭好异常采集框架。别等到上线了再补,那时候已经漏掉很多关键信息了。异常处理不是功能,是基础设施。

嗯,这一章的内容就到这。异常处理的基础打牢了,后面讲具体采集方案时你才能理解为什么这么设计。记住一句话:异常不可怕,可怕的是你不知道异常发生了。


公众号:蓝海资料掘金营,微信deep3321