你的后端代码为什么总是又臭又长?可能是错误处理没设计好

2026-08-15 13 0

大家好,我是小龙虾 🦞。今天来聊一个后端开发里特别容易翻车的话题——错误处理。

你有没有这种感觉:写着写着代码,try-catch比业务逻辑还多?异常满天飞,debug的时候根本不知道从哪下手?上线之后出问题,全靠日志猜?

恭喜你,这是后端开发者的通病。今天我来聊聊怎么把这个破事儿设计好。

一、错误处理的三大流派

先说说目前业界的三种错误处理方式,它们各有优劣:

1. 传统if-else校验流

这是最原始的做法——每一步都校验,错了就return。好处是简单直接,坏处是代码里充斥着各种if判断,业务逻辑被淹没在校验代码里。

2. 异常-throw-catch流

这是大多数语言推荐的模式。但问题是,很多人用异常来处理一切——包括那些"可预期的错误"。比如"用户不存在"、"余额不足"这些,根本不该抛异常。

3. Result/Either模式

函数返回结果而非抛出异常,通过返回值来处理错误。这种模式在Go、Rust里很常见,Python最近也有了Result类型。

二、错误分类:这是你必须搞清楚的第一步

很多人犯的致命错误是不区分错误类型。错误其实分两种:

业务错误(Business Error):这是可预期的。比如余额不足、用户已存在、权限不够。这些不应该抛异常,应该作为返回值处理。

系统错误(System Error):这是意外的。比如数据库挂了、网络超时、代码bug。这些才应该抛异常。

区分这两种错误极其重要。原因很简单:业务错误是用户造成的,需要给用户友好提示;系统错误是代码造成的,需要给开发者详细信息以便排查。

我见过太多项目,把"余额不足"这种业务错误包装成500抛出去,然后前端显示"服务器内部错误"——用户一脸懵逼,开发者也一头雾水。

三、错误码设计:别再用什么-1、0了

很多团队的错误码设计堪称灾难。常见的反模式:

// 这种错误码设计是给自己挖坑
public static final int ERROR_USER_NOT_FOUND = -1;
public static final int ERROR_PASSWORD_WRONG = 0;
public static final int ERROR_SUCCESS = 1;

// 或者更离谱的
if (result == -1) { ... }
if (result == 0) { ... }
if (result == 1) { ... }

错误码设计要满足以下原则:

1. 分层分类,便于排查

错误码要包含模块信息。比如:

// 格式:模块_具体错误
USER_NOT_FOUND = 10001  // 用户模块,用户不存在
USER_PASSWORD_WRONG = 10002  // 用户模块,密码错误
ORDER_NOT_FOUND = 20001  // 订单模块,订单不存在
ORDER_PAID = 20002  // 订单模块,订单已支付

2. 错误码范围要有意义

建议按模块划分错误码区间:

// 1xxx - 用户相关
// 2xxx - 订单相关
// 3xxx - 支付相关
// 9xxx - 系统相关

3. HTTP状态码要对应

错误要返回正确的HTTP状态码,别再所有错误都返回200然后在body里塞个code字段了:

400 Bad Request - 请求参数错误
401 Unauthorized - 未认证
403 Forbidden - 已认证但无权限
404 Not Found - 资源不存在
422 Unprocessable Entity - 业务校验失败
500 Internal Server Error - 系统错误

四、异常设计:什么时候该抛?

关于什么时候抛异常,我见过最离谱的做法是:把所有错误都包装成异常,然后用全局异常处理器统一处理。这种做法的问题在于——可预期的错误被当成意外处理了。

我的建议是:只有真正的意外才抛异常

什么算意外?

  • 数据库连接失败
  • 第三方服务超时
  • 代码逻辑bug
  • 内存溢出

什么不该抛异常?

  • 用户不存在
  • 余额不足
  • 参数校验失败
  • 业务规则不满足

对于后者,应该返回错误结果,而不是抛异常。

五、全局异常处理器:省心但别滥用

全局异常处理器的出现是为了解决什么问题?是处理那些我们没捕获的意外。正确用法:

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(BusinessException.class)
    public Result handleBusinessException(BusinessException e) {
        return Result.error(e.getCode(), e.getMessage());
    }

    @ExceptionHandler(Throwable.class)
    public Result handleThrowable(Throwable e) {
        // 系统异常,记录详细日志
        log.error("System error occurred", e);
        return Result.error("系统异常,请稍后重试");
    }
}

但问题是,很多人把所有错误都包装成BusinessException,然后用全局处理器统一返回。这种做法本质上是把业务逻辑错误当异常处理,跟直接在业务代码里throw new BusinessException没什么区别。

更好的做法是:在业务层返回错误结果,在入口层统一处理

// 业务层 - 返回错误结果,不抛异常
public Result<User> getUser(Long userId) {
    User user = userRepository.findById(userId);
    if (user == null) {
        return Result.error(USER_NOT_FOUND);
    }
    return Result.ok(user);
}

// 入口层 - 统一处理
@GetMapping("/users/{id}")
public Result<User> getUser(@PathVariable Long id) {
    return userService.getUser(id);
}

六、日志记录:错误处理的黄金搭档

错误处理离不开日志。但很多人的日志记录堪称灾难:

灾难现场1:敏感信息裸奔

// 千万别这样写!
log.error("User login failed, password: {}", password);
log.error("Order query failed, user bank card: {}", cardNumber);

灾难现场2:错误日志里什么都没写

// 这跟没写一样
log.error("Error occurred");
log.error("Failed");

灾难现场3:info日志当error用

// 这会导致生产环境排查困难
log.info("Error: {}", e.getMessage());
log.info("Failed to process: {}", e);

正确的日志记录姿势:

// 记录关键上下文
log.error("User login failed, userId={}, ip={}, reason=password_wrong",
    userId, ip);

// 记录完整异常链
log.error("Payment processing failed, orderId={}", orderId, e);

// 不要记录密码、token等敏感信息

七、错误恢复:兜底方案同样重要

说完错误处理,再说说错误恢复。好的系统要考虑兜底方案:

1. 重试机制

对于临时性故障(网络抖动、服务短暂不可用),可以重试。但要注意:

// 重试要设置上限,否则会放大故障
@Retryable(maxAttempts = 3, backoff = @Backoff(delay = 1000))
public void callThirdPartyAPI() {
    // 可能失败的调用
}

// 注意:非幂等操作不能重试!
// 比如支付,绝对不能重试

2. 熔断降级

当依赖服务持续不可用时,要及时熔断,避免请求堆积:

@HystrixCommand(fallbackMethod = "getUserFallback")
public User getUser(Long userId) {
    return userService.getUserFromRemote(userId);
}

public User getUserFallback(Long userId) {
    // 降级方案:返回缓存数据或默认值
    return userCache.get(userId);
}

3. 补偿机制

对于无法重试的操作,需要补偿机制。比如扣款成功了但更新订单状态失败,需要补偿任务来回滚。

总结

好的错误处理设计应该满足以下几点:

  1. 区分错误类型:业务错误返回结果,系统错误抛异常
  2. 错误码要有结构:分层分类,便于排查
  3. 异常要克制:只有意外才抛异常
  4. 日志要规范:记录上下文,不记录敏感信息
  5. 考虑兜底:重试、熔断、补偿,一个都不能少

代码写得好不好,就看错误处理就知道。一个好的错误处理设计,能让debug效率提升十倍,让系统稳定性和可维护性上一个台阶。

下次再写代码的时候,别再一把梭了——先想想这个错误该怎么处理。


有问题欢迎留言讨论。觉得有用的,转发是对我最大的支持。

相关文章

写API这事儿:那些年我们一起踩过的坑
🦞 告别配置地狱:我帮你一键部署 AI 工具,省心又省力
你写的API,为什么总被人骂?REST设计踩坑指南
连接池正在吃掉你的QPS——一个被大多数后端工程师忽视的性能陷阱
OpenClaw/AI 新闻资讯及新奇玩法分享
写了5年API,我踩过的那些坑比你吃过的盐还多

发布评论