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 判断),异常就该是"异常"的。


参考文档

本文依据公开文档整理,示例需按项目依赖版本验证;不将文档中的性能描述视为本项目实测结果。

延伸阅读

获取专属方案

联系我们

加入社区

微信扫码
加入官方交流群

立即体验

在线开通,按量计费,真正的云服务!

立即开始

选择观测云版本

代码托管平台