25、函数设计(下):使用异常替代返回错误码、抽离Try/Catch代码块、别重复自己

上一章我们聊了函数要短、要小、要做一件事。今天继续往下挖。说白了,函数设计的下半场,核心就三个字:别糊弄

错误处理这件事,我见过太多人把它当成“补丁”来写。函数里先写业务逻辑,然后在某个角落塞个 if-else 返回 -1 或者 null。嗯,这种代码我年轻时也写过。后来被坑了几次,才明白——错误处理不是附属品,它本身就是函数设计的一部分

用异常替代返回错误码

先看一个反面教材。这是我早期项目里真实出现过的代码:

// 错误码风格
public int saveUser(User user) {
    if (user == null) {
        return -1; // 用户为空
    }
    if (user.getName() == null || user.getName().isEmpty()) {
        return -2; // 名字不合法
    }
    // 保存逻辑...
    return 0; // 成功
}

调用方呢?更惨:

int result = saveUser(user);
if (result == -1) {
    // 处理空用户
} else if (result == -2) {
    // 处理名字问题
} else if (result == 0) {
    // 正常流程
}

你想想看,这代码读起来像什么?像在查一本错误码字典。而且最要命的是——调用方很容易忘记检查返回值。我踩过这个坑,有一次线上数据丢了,查了半天,发现就是某个函数返回了 -3,但调用方直接忽略了,继续往下执行。

⚠️ 我曾经犯过的错: 用返回码 + 全局变量 errno 的方式处理错误。结果多线程环境下 errno 被覆盖,排查了整整两天。从那以后,我再也不在业务代码里用返回码了。

正确的做法是什么?用异常:

public void saveUser(User user) {
    if (user == null) {
        throw new IllegalArgumentException("用户对象不能为空");
    }
    if (user.getName() == null || user.getName().isEmpty()) {
        throw new IllegalArgumentException("用户名不能为空");
    }
    // 保存逻辑...
}

调用方只需要在合适的地方 catch 一次:

try {
    userService.saveUser(user);
} catch (IllegalArgumentException e) {
    // 统一处理参数错误
    logger.warn("保存用户参数异常", e);
    return errorResponse(e.getMessage());
}

好处很明显:主流程和错误处理分开了。代码不再被 if-else 割得支离破碎。我个人习惯是,业务异常用自定义异常类,系统异常用 RuntimeException 的子类。这样 catch 的时候能精确区分。

抽离 Try/Catch 代码块

好,现在你开始用异常了。但别高兴太早——很多人又掉进另一个坑:把 try/catch 写得满屏都是

来看这个:

public void processOrder(Order order) {
    try {
        // 验证订单
        validateOrder(order);
        // 计算价格
        BigDecimal price = calculatePrice(order);
        // 扣减库存
        deductStock(order);
        // 发送通知
        sendNotification(order);
    } catch (ValidationException e) {
        logger.error("订单验证失败", e);
        throw e;
    } catch (PriceException e) {
        logger.error("价格计算异常", e);
        throw e;
    } catch (StockException e) {
        logger.error("库存扣减失败", e);
        throw e;
    }
}

这代码有什么问题?try 块里塞了太多事。你想想看,如果 validateOrder 抛异常,后面的 calculatePrice 根本不会执行。但读代码的人需要从头到尾看完整个 try 块,才能理解“哦,原来这些步骤是顺序执行的”。

我建议的做法是:把 try/catch 抽成一个单独的函数

public void processOrder(Order order) {
    try {
        doProcessOrder(order);
    } catch (ValidationException | PriceException | StockException e) {
        logger.error("订单处理失败", e);
        throw new OrderProcessException("处理订单异常", e);
    }
}

private void doProcessOrder(Order order) {
    validateOrder(order);
    BigDecimal price = calculatePrice(order);
    deductStock(order);
    sendNotification(order);
}

看到了吗?doProcessOrder 只做业务逻辑,没有错误处理的干扰。而 processOrder 只负责错误处理,不关心具体业务。这就是单一职责原则在错误处理上的体现。

💡 我的经验: 一个函数里如果 try 块超过 10 行,就该考虑抽离了。把 try 块里的逻辑提取成一个私有方法,命名就叫 doXxx 或者 executeXxx,清晰明了。

别重复自己(DRY 原则)

这个原则说起来简单,做起来难。我见过最夸张的一个项目,同一个“用户不存在”的错误处理逻辑,在 12 个地方重复出现。后来需求变了,要改成“用户不存在时发送短信提醒”,改代码的人差点崩溃。

重复的错误处理长什么样?

// Controller A
try {
    User user = userService.findById(id);
    if (user == null) {
        return ResponseEntity.status(404).body("用户不存在");
    }
    // ...
} catch (Exception e) {
    return ResponseEntity.status(500).body("系统错误");
}

// Controller B
try {
    User user = userService.findById(id);
    if (user == null) {
        return ResponseEntity.status(404).body("用户不存在");
    }
    // ...
} catch (Exception e) {
    return ResponseEntity.status(500).body("系统错误");
}

这代码有什么问题?一模一样的逻辑写了两次。如果哪天要把 404 改成 400,你得改两个地方。万一漏了一个呢?

正确的做法是:把重复的错误处理逻辑抽象成公共方法或全局异常处理器

// 全局异常处理器
@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(UserNotFoundException.class)
    public ResponseEntity<String> handleUserNotFound(UserNotFoundException e) {
        return ResponseEntity.status(404).body(e.getMessage());
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<String> handleGeneralError(Exception e) {
        logger.error("系统异常", e);
        return ResponseEntity.status(500).body("系统繁忙,请稍后重试");
    }
}

这样,每个 Controller 只需要抛出对应的异常,错误处理由全局统一接管。代码量减少,维护成本降低,而且错误处理的行为是一致的

核心要点:

  • 用异常替代返回码,让主流程更干净
  • 抽离 try/catch 块,让错误处理和业务逻辑分离
  • 消除重复的错误处理代码,用统一机制管理

本章知识体系

下面这张图总结了函数设计中错误处理的三个核心原则,以及它们之间的关系:

函数设计中的错误处理原则 错误处理设计 原则一:用异常替代返回码 • 避免 if-else 嵌套地狱 • 防止调用方忘记检查 • 主流程更清晰 原则二:抽离 Try/Catch • 业务逻辑与错误处理分离 • 单一职责原则 • 函数更短更专注 原则三:别重复自己 • 统一异常处理机制 • 全局异常处理器 • 降低维护成本 最终目标 函数只做一件事,错误处理不干扰主流程,重复代码被消除

这三个原则其实是递进关系。先用异常替代返回码,让错误能被“抛出”而不是“返回”;然后把 try/catch 抽离出来,让错误处理不污染业务逻辑;最后用 DRY 原则消除重复,让整个系统的错误处理风格统一。

我个人觉得,错误处理是衡量一个工程师是否成熟的重要标志。新手往往只关注“正常流程怎么走”,而老手会花同样多的精力思考“出错了怎么办”。

嗯,今天就到这里。记住:好的代码,连错误处理都是优雅的


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