Ruby 异常处理完全指南:rescue、ensure 与自定义异常
本文系统讲解 Ruby 异常体系:Exception 类层级、begin/rescue/else/ensure 的语义、raise 抛出与自定义异常类的最佳实践,以及生产环境如何结合监控平台管理未捕获异常。
Ruby 用 begin/rescue/else/ensure 四块结构处理异常:rescue 捕获指定错误,else 在无异常时执行,ensure 无论如何都跑。自定义异常继承 StandardError(不要继承 Exception),rescue 时永远指明具体异常类而不是裸 rescue。
异常即对象
Ruby 中错误是对象:问题发生时创建一个异常实例,沿调用栈向上冒泡,遇到匹配的 rescue 被处理,否则终止程序。理解这个模型,就理解了为什么 rescue 要按继承关系排序、为什么 e.message 和 e.backtrace 总是可用。
异常类层级:该捕获哪一层
Exception
├── NoMemoryError / SignalException / SystemExit ← 别碰这些
└── StandardError ← 应用异常的根
├── ArgumentError / TypeError
├── RuntimeError
├── IOError / Errno::*(系统调用错误)
└── Net::HTTPError / JSON::ParserError 等
黄金法则:只 rescue StandardError 及其子类。 裸 rescue 默认也只捕获 StandardError,但显式写出具体异常类是更好的习惯——捕获面越窄,误吞 bug 的可能越小。直接继承 Exception 的自定义异常不会被默认的 StandardError rescue 捕获;宽泛地写 rescue Exception 才会把 SystemExit 等退出信号一起捕获,是经典灾难。
rescue 的完整语法
def fetch_report(id)
data = api.get("/reports/#{id}")
result = JSON.parse(data.body)
rescue Net::OpenTimeout, Net::ReadTimeout => e
logger.warn("接口超时:#{e.message}")
nil
rescue JSON::ParserError => e
logger.error("返回格式异常:#{e.message}")
raise ReportFormatError, "报表服务返回非法 JSON"
else
result # else 的返回值会成为方法返回值
ensure
api.close if api
end
rescue A, B => e:捕获多类异常并绑定变量retry:在 rescue 中重试(务必加重试次数上限,否则死循环)- 方法级简写:方法体自带隐式 begin,直接写 rescue 即可
- 修饰符写法:
value = risky_call rescue default,一行兜底,但吞掉所有 StandardError,只在明确无害时用
raise 与自定义异常
class InsufficientFundsError < StandardError
attr_reader :balance
def initialize(balance)
@balance = balance
super("余额不足:当前 #{balance}")
end
end
raise InsufficientFundsError.new(account.balance) if account.balance < amount
携带上下文数据(这里是 balance)让上层日志和监控告警更有价值。raise 不传参数时重抛当前异常,常用于"记录日志后继续上抛"的场景。
有限重试与异常堆栈
retry 会重新执行对应的 begin 块。只对可以安全重试的临时失败使用它,并设置次数上限和退避;读文件重试还要处理已经移动的文件位置。不能盲目重试支付或写入操作。
Ruby 的 e.backtrace 通常从异常发生位置向外列出调用者,先看 e.backtrace.first,再沿调用栈追查;e.cause 可帮助追溯被包装的异常。
ensure:资源释放的保险
文件句柄、数据库连接、分布式锁这类资源必须在 ensure 中释放。一个反模式要警惕:ensure 里写 return 会静默吞掉正在冒泡的异常,这是 Ruby 最隐蔽的 bug 来源之一。
保留异常上下文并关联 Ruby 请求
rescue 之后,仍要保留足够的信息解释失败:异常类别、脱敏后的堆栈、业务操作和部署版本。通过 DataKit 采集日志后,可在观测云中检查错误分布;Ruby APM 则补充已插桩请求的调用过程。日志与链路要按文档注入关联字段,被业务 rescue 消化的异常也需要显式记录,不能假设探针会捕获所有失败。
接入文档:日志采集 · Ruby APM 接入 · 日志与链路关联 · 监控器与告警策略。
常见问题(FAQ)
Q:rescue 后应该返回 nil 还是重抛?
取决于调用方能否处理失败。能用默认值安全降级就返回兜底值;调用方必须知道失败事实(如支付、写库)就重抛或抛更语义化的自定义异常。最差的选择是 rescue 后静默吞掉、什么都不做。
Q:Rails 里在哪里统一处理异常?
控制器层用 rescue_from 按异常类映射响应(如 rescue_from ActiveRecord::RecordNotFound, with: :render_404);全局兜底在 config.exceptions_app。模型和 Service 层不要 rescue 了自己悄悄处理,让异常浮到边界再统一应对。
Q:begin/rescue 影响性能吗?
不抛异常时开销可忽略;异常真正抛出时构建 backtrace 较昂贵。所以不要用异常做正常流程控制(比如用 rescue 替代 nil 判断),异常就该是"异常"的。
参考文档
本文依据公开文档整理,示例需按项目依赖版本验证;不将文档中的性能描述视为本项目实测结果。