错误处理与异常:统一异常处理、错误码设计、友好错误提示、重试机制
说实话,后端开发里最让人头疼的,往往不是业务逻辑有多复杂,而是那些你预料不到的异常。我记得刚带团队那会儿,线上一个接口突然返回了500,前端直接弹了个「服务器开小差」——用户一脸懵,我们也一脸懵。从那以后,我就在想:能不能让错误变得「可预测」、「可解释」、「可恢复」?
这一章,我们就来聊聊Ktor里怎么把异常处理做成体系。说白了,就是三件事:统一收口、友好提示、自动重试。
一、统一异常处理:别让异常「裸奔」
Ktor默认的异常处理是分散的。你可以在每个路由里写try-catch,但那样太累了。我个人习惯用StatusPages插件来做全局拦截。
install(StatusPages) {
exception<Throwable> { cause ->
call.respond(HttpStatusCode.InternalServerError, mapOf(
"code" to 500,
"message" to "服务器内部错误"
))
}
}
嗯,这里要注意:exception<Throwable>会捕获所有异常。但如果你有自定义异常,最好单独处理,别一股脑全吞了。
核心原则:越具体的异常,优先级越高。Ktor会按注册顺序匹配,所以把通用异常放最后。
二、错误码设计:给异常一个「身份证」
我在项目中遇到过一个问题:前端拿到错误后,只能根据message做if-else判断。后来我改成用错误码,前端直接查表,清晰多了。
错误码设计,我建议分三段:
- 模块码(2位):比如01表示用户模块,02表示订单模块
- 错误类型码(2位):比如01表示参数错误,02表示权限不足
- 具体错误码(2位):从01开始递增
| 错误码 | 含义 | HTTP状态码 |
|---|---|---|
| 010101 | 用户模块-参数错误-用户名不能为空 | 400 |
| 010201 | 用户模块-权限不足-未登录 | 401 |
| 020101 | 订单模块-参数错误-订单号格式错误 | 400 |
sealed class ApiError(val code: String, val message: String) {
class UserNameEmpty : ApiError("010101", "用户名不能为空")
class NotLogin : ApiError("010201", "请先登录")
class OrderIdInvalid : ApiError("020101", "订单号格式错误")
}
你想想看,前端拿到010101,直接就能定位到是哪个模块、什么类型、具体哪个字段出了问题。比看一段中文message靠谱多了。
三、友好错误提示:别让用户看「天书」
我曾经见过一个接口返回:NullPointerException at line 47。这玩意儿用户看了能干嘛?
友好提示,我总结了三层:
- 用户层:简单、可理解的话,比如「网络开小差,请稍后重试」
- 开发者层:详细的错误信息,比如错误码、堆栈、请求ID
- 日志层:完整的异常堆栈,方便排查
exception<ApiError> { error ->
call.respond(HttpStatusCode.BadRequest, mapOf(
"code" to error.code,
"message" to error.message,
"detail" to "如需帮助,请联系客服并提供错误码"
))
}
小技巧:在响应头里加一个X-Request-Id,用户反馈问题时,你直接查日志就能定位到那次请求。
四、重试机制:给「临时故障」一次机会
有些异常是临时的,比如网络抖动、数据库连接池满。直接返回失败太可惜了。我建议用重试机制来兜底。
Ktor客户端自带重试支持,但服务端处理外部调用时,也需要自己实现重试逻辑。
suspend fun <T> retryWithBackoff(
maxRetries: Int = 3,
initialDelay: Long = 100L,
block: suspend () -> T
): T {
var currentDelay = initialDelay
repeat(maxRetries) { attempt ->
try {
return block()
} catch (e: Exception) {
if (attempt == maxRetries - 1) throw e
delay(currentDelay)
currentDelay *= 2 // 指数退避
}
}
throw IllegalStateException("不可达")
}
为什么用指数退避?你想想看,如果服务端正在恢复,你疯狂重试只会加重负担。等一会儿再试,反而成功率更高。
注意:重试只适用于幂等操作。比如查询、删除可以重试,但创建订单、扣款这种操作,重试前一定要做幂等校验。
五、整体架构:一张图看懂
我把整个异常处理体系画成了流程图,方便你理解各个组件的关系。
从图上你能看到:请求进来后,先经过StatusPages拦截器。根据异常类型走不同分支——自定义异常返回错误码,临时异常触发重试,其他异常兜底返回500。最终所有响应都统一格式输出。
六、实战建议
最后,我分享几个实战中踩过的坑:
- 不要吞异常:我曾经为了省事,在catch块里直接返回null。结果排查问题时完全不知道发生了什么。一定要记录日志。
- 重试要有上限:无限重试等于拒绝服务。我一般设3次,每次间隔翻倍。
- 错误码要文档化:把错误码和对应的含义整理成文档,前后端各存一份。沟通成本能降一半。
我的习惯:在开发环境,异常信息返回完整堆栈;生产环境只返回错误码和友好提示。用environment判断环境,一行代码搞定。
异常处理这件事,说白了就是「防患于未然」。你提前想好了怎么收口、怎么提示、怎么恢复,线上出问题时就不会手忙脚乱。嗯,这一章的内容就到这里,希望对你有所帮助。
公众号:蓝海资料掘金营,微信deep3321