# Java异常处理这几个坑,我踩了三年才搞明白

5 阅读2分钟

刚工作那会儿,我写的catch块只有一行:e.printStackTrace()。后来线上出问题,日志里啥也看不出来,才知道异常处理这事儿没那么简单。

坑一:catch住不处理

try {
    orderService.create(order);
} catch (Exception e) {
    // 啥也不写
}

这是最致命的写法。异常被吞掉,程序继续往下跑,数据可能已经处于不一致状态,而你完全不知道。

正确的做法至少要做三件事之一:记录日志、向上抛出、或者做降级处理。

坑二:catch Exception一把梭

try {
    // 业务逻辑
} catch (Exception e) {
    log.error("出错了", e);
}

Exception太宽了,你根本不知道发生了什么。业务异常应该被区分对待——参数校验失败、库存不足,这些是预期内的,不需要报警;而NPE、数据库连接失败,这些才是真要命的。

try {
    orderService.create(order);
} catch (BizException e) {
    // 业务异常,返回友好提示
    return Result.fail(e.getMessage());
} catch (Exception e) {
    // 系统异常,记录完整堆栈并报警
    log.error("创建订单失败, order={}", order, e);
    return Result.fail("系统繁忙,请稍后重试");
}

坑三:finally里return

public int getValue() {
    try {
        return 1;
    } finally {
        return 2;  // 覆盖了try里的return
    }
}
// 返回2,不是1

finally里的return会覆盖try里的返回值,而且会吞掉try中抛出的异常。这属于"语法允许但绝对不要写"的范畴。

坑四:在finally里关闭资源却不判空

Connection conn = null;
try {
    conn = getConnection();
    // ...
} finally {
    conn.close();  // conn可能为null,抛NPE
}

现在都用try-with-resources了,这个问题自然解决:

try (Connection conn = getConnection();
     PreparedStatement ps = conn.prepareStatement(sql)) {
    // 自动关闭,异常也能正确传播
}

坑五:自定义异常丢了cause

// 错误写法
try {
    parseConfig();
} catch (IOException e) {
    throw new ConfigException("配置加载失败");
}

// 正确写法:保留原始异常
try {
    parseConfig();
} catch (IOException e) {
    throw new ConfigException("配置加载失败", e);
}

不传cause,堆栈就断在这里了,排查问题时只能看到"配置加载失败",看不到真正的IOException在哪一行。

我的几条实践原则

原则说明
不要吞异常至少记日志
不要catch Exception精确捕获,分别处理
不要用异常控制流程能用if判断就别抛异常
异常信息要带上下文把关键参数打进日志
自定义异常保留cause别断掉堆栈链

异常处理写得好不好,平时看不出来,一到线上排障就高下立判。共勉。