强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

第 12 章:异常处理

第 12 章:异常处理

“错误是程序的常态,优雅地处理它们是程序员的修养。”


12.1 异常基础

12.1.1 异常类层次

Exception
├── NoMemoryError
├── ScriptError
│   ├── LoadError
│   ├── NotImplementedError
│   ├── SyntaxError
│   └── SystemExit
├── SecurityError
├── SignalException
│   └── Interrupt
├── StandardError(默认 rescue 捕获这个)
│   ├── ArgumentError
│   ├── IOError
│   ├── IndexError
│   ├── KeyError
│   ├── NameError
│   │   └── NoMethodError
│   ├── RangeError
│   │   └── FloatDomainError
│   ├── RegexpError
│   ├── RuntimeError
│   ├── TypeError
│   └── ZeroDivisionError
└── SystemCallError
    ├── Errno::EACCES
    ├── Errno::ENOENT
    └── ...

12.1.2 begin / rescue / end

# 基本异常处理
begin
  result = 10 / 0
rescue ZeroDivisionError => e
  puts "除零错误: #{e.message}"
end

# 捕获多种异常
begin
  # 可能出错的代码
  data = JSON.parse(input)
  data.fetch("required_key")
rescue JSON::ParserError => e
  puts "JSON 解析错误: #{e.message}"
rescue KeyError => e
  puts "键不存在: #{e.message}"
end

# 捕获所有异常(不推荐,应该明确捕获)
begin
  risky_operation
rescue => e
  puts "发生错误: #{e.class} - #{e.message}"
end

# 捕获 Exception(非常不推荐,会捕获系统信号)
begin
  risky_operation
rescue Exception => e
  # 这会捕获 Interrupt、SystemExit 等
end

12.1.3 异常对象

begin
  1 / 0
rescue => e
  e.class        # => ZeroDivisionError
  e.message      # => "divided by 0"
  e.backtrace    # => 堆栈追踪数组
  e.cause        # => 原始异常(如果有)
end

12.2 ensure 与 else

12.2.1 ensure(确保执行)

# ensure 无论是否发生异常都会执行
def read_file(path)
  file = File.open(path)
  content = file.read
  content
rescue Errno::ENOENT => e
  puts "文件不存在: #{e.message}"
  nil
rescue Errno::EACCES => e
  puts "没有权限: #{e.message}"
  nil
ensure
  file&.close  # 确保文件关闭
  puts "资源已清理"
end

12.2.2 else(无异常时执行)

begin
  result = calculate_something
rescue => e
  puts "错误: #{e.message}"
else
  puts "计算成功: #{result}"  # 只在没有异常时执行
ensure
  puts "清理工作"
end

# 执行顺序:
# 1. begin 块
# 2. 有异常 → rescue
# 3. 无异常 → else
# 4. 总是 → ensure

12.2.3 方法中的异常处理

# 方法本身就是 begin 块
def divide(a, b)
  a / b
rescue ZeroDivisionError
  puts "不能除以零"
  nil
ensure
  puts "计算完成"
end

# 也可以显式使用
def process(data)
  begin
    validate(data)
    transform(data)
  rescue ValidationError => e
    log_error(e)
    raise  # 重新抛出
  end
end

12.3 raise 与 retry

12.3.1 raise 抛出异常

# 抛出异常
raise "Something went wrong"
raise ArgumentError, "Invalid argument: #{arg}"
raise TypeError.new("Expected String")

# 重新抛出当前异常
begin
  risky_operation
rescue => e
  log_error(e)
  raise  # 重新抛出相同的异常
end

# 转换异常
begin
  external_api_call
rescue HTTPError => e
  raise ServiceError, "API 调用失败: #{e.message}"
end

12.3.2 retry 重试

# 基本重试
attempts = 0
begin
  attempts += 1
  puts "尝试第 #{attempts} 次..."
  connect_to_database
rescue ConnectionError => e
  if attempts < 3
    sleep(1)
    retry  # 重试 begin 块
  else
    puts "连接失败,已重试 #{attempts} 次"
    raise
  end
end

# 带延迟的重试
def with_retry(max_attempts: 3, delay: 1)
  attempts = 0
  begin
    attempts += 1
    yield
  rescue StandardError => e
    if attempts < max_attempts
      sleep(delay * attempts)  # 指数退避
      retry
    else
      raise
    end
  end
end

with_retry(max_attempts: 3, delay: 2) do
  external_service.call
end

12.3.3 重试模式

# 指数退避重试
def with_exponential_backoff(max_attempts: 5)
  attempts = 0
  begin
    attempts += 1
    yield
  rescue StandardError => e
    if attempts < max_attempts
      delay = 2 ** attempts + rand(0..1)
      puts "重试 #{attempts}/#{max_attempts},等待 #{delay}s..."
      sleep(delay)
      retry
    else
      raise e
    end
  end
end

# 只重试特定异常
def with_selective_retry(max_attempts: 3, retry_on: [StandardError])
  attempts = 0
  begin
    attempts += 1
    yield
  rescue *retry_on => e
    if attempts < max_attempts
      sleep(1)
      retry
    else
      raise
    end
  end
end

with_selective_retry(retry_on: [Timeout::Error, ConnectionError]) do
  api_call
end

12.4 自定义异常

12.4.1 定义异常类

# 基本自定义异常
class AppError < StandardError; end

class ValidationError < AppError
  attr_reader :field, :value

  def initialize(field, value, message = nil)
    @field = field
    @value = value
    super(message || "Invalid value for #{field}: #{value}")
  end
end

class NotFoundError < AppError
  attr_reader :resource, :id

  def initialize(resource, id)
    @resource = resource
    @id = id
    super("#{resource} not found: #{id}")
  end
end

# 使用
def find_user(id)
  user = User.find_by(id: id)
  raise NotFoundError.new("User", id) unless user
  user
end

begin
  find_user(999)
rescue NotFoundError => e
  puts e.message  # => "User not found: 999"
  puts "Resource: #{e.resource}"
  puts "ID: #{e.id}"
end

12.4.2 异常层次设计

# 应用级异常层次
module MyApp
  class Error < StandardError; end
  
  # 认证相关
  class AuthenticationError < Error; end
  class AuthorizationError < Error; end
  class TokenExpiredError < AuthenticationError; end
  
  # 数据相关
  class ValidationError < Error
    attr_reader :errors

    def initialize(errors = {})
      @errors = errors
      super(errors.values.flatten.join(", "))
    end
  end
  
  class RecordNotFoundError < Error
    attr_reader :model, :id
    
    def initialize(model, id)
      @model = model
      @id = id
      super("#{model}##{id} not found")
    end
  end
  
  # 服务相关
  class ServiceError < Error; end
  class ExternalServiceError < ServiceError
    attr_reader :service, :response
    
    def initialize(service, response = nil)
      @service = service
      @response = response
      super("External service error: #{service}")
    end
  end
end

# 捕获特定或父类异常
begin
  do_something
rescue MyApp::AuthenticationError => e
  # 处理所有认证错误(包括 TokenExpiredError)
rescue MyApp::ValidationError => e
  # 处理验证错误
  puts e.errors.inspect
rescue MyApp::Error => e
  # 处理所有应用错误
end

12.5 内核方法

12.5.1 raise 方法

# raise 是 Kernel 方法,可以在任何地方调用
def validate!(value)
  raise ArgumentError, "Value cannot be nil" if value.nil?
  raise ArgumentError, "Value must be positive" if value <= 0
  true
end

# 条件 raise
def process(data)
  raise "Data is empty" if data.empty?
  raise TypeError unless data.is_a?(Array)
  data.map(&:to_s)
end

12.5.2 fail 方法

# fail 和 raise 完全相同
fail "Error message"
fail ArgumentError, "Bad argument"

# 约定:fail 表示不应该恢复的错误,raise 表示可以 rescue 的异常
# 但实际上 Ruby 社区通常统一使用 raise

12.6 catch 与 throw

12.6.1 catch/throw 控制流

# catch/throw 不是异常处理,而是控制流机制
def find_value(matrix, target)
  matrix.each_with_index do |row, i|
    row.each_with_index do |val, j|
      throw(:found, { value: val, row: i, col: j }) if val == target
    end
  end
  nil
end

matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]

result = catch(:found) do
  find_value(matrix, 5)
end

puts result  # => { value: 5, row: 1, col: 1 }

12.6.2 catch/throw vs begin/rescue

# catch/throw 用于正常的控制流跳出
# begin/rescue 用于异常情况

# catch/throw 示例:深层嵌套跳出
def deep_search(data)
  data.each do |key, value|
    if value.is_a?(Hash)
      deep_search(value)
    elsif value == "target"
      throw(:found, key)
    end
  end
end

data = {
  a: { b: { c: "other" } },
  d: { e: "target" }
}

result = catch(:found) { deep_search(data) }
puts result  # => :d

12.7 实际业务场景

12.7.1 API 错误处理

module API
  class ErrorHandler
    ERROR_MAP = {
      "RecordNotFound"    => [404, "资源不存在"],
      "ValidationError"   => [422, "数据验证失败"],
      "Unauthorized"      => [401, "未授权"],
      "Forbidden"         => [403, "禁止访问"],
      "RateLimited"       => [429, "请求过于频繁"]
    }.freeze

    def self.handle(error)
      status, message = ERROR_MAP.fetch(error.class.name, [500, "服务器内部错误"])
      
      {
        status: status,
        error: {
          type: error.class.name,
          message: message,
          details: error.respond_to?(:details) ? error.details : nil
        }
      }
    end
  end
end

# 在应用中使用
begin
  user = User.find(params[:id])
  render json: user
rescue RecordNotFound => e
  render json: API::ErrorHandler.handle(e), status: 404
rescue ValidationError => e
  render json: API::ErrorHandler.handle(e), status: 422
rescue => e
  logger.error(e)
  render json: { error: "Internal server error" }, status: 500
end

12.7.2 事务处理

class Transaction
  def self.execute
    result = nil
    begin
      ActiveRecord::Base.transaction do
        result = yield
      end
    rescue ActiveRecord::RecordInvalid => e
      log_error("Transaction failed: #{e.message}")
      raise
    rescue => e
      log_error("Unexpected error: #{e.message}")
      raise
    end
    result
  end
end

Transaction.execute do
  user = User.create!(name: "Alice")
  order = Order.create!(user: user, total: 100)
  Payment.process!(order)
end

12.7.3 资源清理

# 确保资源释放
def with_resource
  resource = acquire_resource
  yield resource
ensure
  resource&.release
end

# 临时文件处理
require "tempfile"

def with_temp_file(content)
  file = Tempfile.new(["output", ".txt"])
  file.write(content)
  file.rewind
  yield file
ensure
  file&.close
  file&.unlink
end

with_temp_file("Hello") do |f|
  puts f.read
end

12.8 动手练习

  1. 实现 Circuit Breaker
class CircuitBreaker
  # 实现熔断器模式
  # 当连续失败超过阈值时,停止调用
end
  1. 实现异常重试装饰器
def with_retry(max: 3, on: [StandardError], backoff: 1)
  # 实现带退避的重试机制
end
  1. 实现异常聚合
class AggregateError < StandardError
  # 收集多个异常并统一报告
end

12.9 本章小结

要点说明
begin/rescue基本异常捕获结构
ensure无论是否异常都执行的清理代码
else没有异常时执行的代码
raise抛出异常
retry重试 begin 块
自定义异常继承 StandardError,设计清晰的异常层次
catch/throw控制流机制,非异常处理

📖 扩展阅读


上一章← 第 11 章:元编程 下一章第 13 章:模块深入 →