程序人生

写优雅的程序,做优雅的人

怎样才是 ruby 中捕获异常的正确姿势?

| Comments

在开发和使用微服务间通信组件 SneakersPacker 过程中遇到异常处理的一些问题,通过解决这些问题让我对 ruby 中捕获异常和定义异常有新的认识。

当初遇到的问题是这样的:薄荷的一个子系统 status 中原本使用 http api call 获取 record 子系统的一些数据,应用 SneakersPacker 之后,把代码改成了 rpc call。刚开始的时候 SneakersPacker 还有一些问题,为了防止 rpc call 出状况,在代码中提供了一层保护机制,也就是当 SneakersPacker.remote_call 触发异常之后,通过 http api call 获取数据。代码很简单,如下所示:

1
2
3
4
5
  begin
    res = SneakersPacker.remote_call "record.get_simple_profile", params_data
  rescue
    res = HTTPProxy::Record.get "api/v1/users/simple_profile", params_data
  end

但是实际运行结果很让人诧异,remote_call 的确触发了异常,但是代码中的 rescue 并没有捕获到异常,而是直接导致程序 500 错误,大量异常记录在日志和监控系统中,让人百思不得其解。在那之前我以为孤立的 rescue 会捕获全部异常的,仔细研究网络上的一些文档后,发现并不是这样,rescue 其实只捕获 StandardError 类型的异常。再去查阅 SneakersPacker 的代码,发现原来报了很多的异常 RemoteCallTimeoutError 是这样定义的:

1
  class RemoteCallTimeoutError < Exception; end

RemoteCallTimeoutError 选择父类 Exception 是错误的,应该选择 StandardError,改成下面之后 RemoteCallTimeoutError 象期望的一样被捕获了。

1
  class RemoteCallTimeoutError < StandardError; end

为了说明 rescue 这个关键字的行为,我们看下面几个例子:

1
2
3
4
5
6
# 代码1
begin
  success_job
rescue
  fail_job
end
1
2
3
4
5
6
7
# 代码2
begin
  success_job
rescue => e
  fail_job
  puts e is #{e.inspect}"
end
1
2
3
4
5
6
7
# 代码3
begin
  success_job
rescue StandardError => e
  fail_job
  puts e is #{e.inspect}"
end
1
2
3
4
5
6
7
# 代码4
begin
  success_job
rescue Exception => e
  fail_job
  puts e is #{e.inspect}"
end

代码1,代码2和代码3的 rescue 都只捕获 StandardError 类别的异常,而代码4明显不同,代码4会捕获所有的异常。

一些直接继承自 Exception 而不是 StandardError 的异常如下,都是系统层级严重错误,不应该由应用捕获。

1
2
3
4
5
6
7
8
9
10
11
12
SystemStackError
NoMemoryError
SecurityError
ScriptError
  NotImplementedError
  LoadError
    Gem::LoadError
  SyntaxError
SignalException
  Interrupt
SystemExit
  Gem::SystemExitException

综上所述,总结一些 ruby 中捕获异常的原则(正确姿势)

  1. 最好的 rescue 姿势,捕获明确指定的异常,rescue OneError => e
  2. 次好的 rescue 姿势,捕获 StandardError,通过 rescue 或 rescue => e
  3. 千万千万不要 rescue Exception => e,除非真的知道自己在干嘛
  4. 自己定义的异常应该继承自 StandardError ,而不是 Exception。

end.

Comments