Skip to content

Ruby 异常处理

异常处理是编写健壮程序的重要组成部分。在程序运行过程中,可能会遇到各种预期外的情况,如文件不存在、网络连接失败、无效输入等。Ruby提供了完善的异常处理机制,帮助开发者优雅地处理这些情况,提高程序的稳定性和用户体验。本章将详细介绍Ruby中异常处理的各种方法和最佳实践。

🎯 异常基础

什么是异常

异常是在程序执行过程中发生的不正常情况,它会中断程序的正常执行流程。Ruby中的异常是对象,继承自[Exception](file:///D:/Workspace/Coding/VueProjects/tutorials-web/docs/ruby/../../../../../../Ruby30-x64/lib/ruby/3.0.0/exception.rb#L1-L338)类或其子类。

ruby
# 基本异常处理
begin
  # 可能出错的代码
  result = 10 / 0
rescue
  # 处理异常
  puts "发生错误了!"
end

# 更具体的异常处理
begin
  result = 10 / 0
rescue ZeroDivisionError
  puts "不能除以零!"
end

# 捕获异常对象
begin
  result = 10 / 0
rescue ZeroDivisionError => e
  puts "错误类型: #{e.class}"
  puts "错误信息: #{e.message}"
  puts "错误追踪: #{e.backtrace}"
end

Ruby异常层次结构

ruby
# Ruby异常类层次结构(部分)
# Exception
#   ├── NoMemoryError
#   ├── ScriptError
#   │     ├── LoadError
#   │     ├── NotImplementedError
#   │     └── SyntaxError
#   ├── SignalException
#   │     └── Interrupt
#   ├── StandardError  # 最常用的异常基类
#   │     ├── ArgumentError
#   │     ├── IOError
#   │     ├── IndexError
#   │     ├── LocalJumpError
#   │     ├── NameError
#   │     │     └── NoMethodError
#   │     ├── RangeError
#   │     ├── RegexpError
#   │     ├── RuntimeError
#   │     ├── SecurityError
#   │     ├── StandardError
#   │     ├── StopIteration
#   │     ├── SyntaxError
#   │     ├── SystemCallError
#   │     ├── SystemStackError
#   │     ├── ThreadError
#   │     ├── TypeError
#   │     └── ZeroDivisionError
#   ├── SystemExit
#   └── fatal

# 常见异常类型示例
begin
  # ArgumentError - 参数错误
  def greet(name)
    raise ArgumentError, "名字不能为空" if name.nil? || name.empty?
    puts "你好, #{name}!"
  end
  
  greet("")  # 会抛出ArgumentError
  
rescue ArgumentError => e
  puts "参数错误: #{e.message}"
end

begin
  # TypeError - 类型错误
  result = "hello" + 5  # 会抛出TypeError
  
rescue TypeError => e
  puts "类型错误: #{e.message}"
end

begin
  # NoMethodError - 方法不存在错误
  obj = "hello"
  obj.nonexistent_method  # 会抛出NoMethodError
  
rescue NoMethodError => e
  puts "方法错误: #{e.message}"
end

🚨 异常处理语法

基本异常处理结构

ruby
# 完整的异常处理结构
begin
  # 可能出错的代码
  puts "开始执行"
  raise "这是一个自定义错误"
  puts "这行不会执行"
  
rescue StandardError => e
  # 处理标准异常
  puts "捕获到异常: #{e.message}"
  
rescue Exception => e
  # 处理所有异常(不推荐)
  puts "捕获到所有异常: #{e.message}"
  
else
  # 没有异常时执行
  puts "没有发生异常"
  
ensure
  # 无论是否有异常都会执行
  puts "清理工作"
end

多重异常处理

ruby
# 处理多种异常类型
begin
  # 可能抛出不同异常的代码
  file = File.open("nonexistent.txt")
  data = file.read
  result = 10 / 0
  
rescue Errno::ENOENT => e
  # 文件不存在错误
  puts "文件不存在: #{e.message}"
  
rescue ZeroDivisionError => e
  # 除零错误
  puts "除零错误: #{e.message}"
  
rescue IOError => e
  # IO错误
  puts "IO错误: #{e.message}"
  
rescue StandardError => e
  # 其他标准错误
  puts "其他错误: #{e.message}"
end

# 使用数组处理多种异常
begin
  # 可能出错的代码
  raise ArgumentError, "参数错误"
  
rescue [ArgumentError, TypeError, NameError] => e
  puts "捕获到类型相关的错误: #{e.class} - #{e.message}"
end

异常重新抛出和嵌套

ruby
# 重新抛出异常
begin
  begin
    raise "内部错误"
  rescue => e
    puts "内部捕获: #{e.message}"
    # 重新抛出异常
    raise
  end
  
rescue => e
  puts "外部捕获: #{e.message}"
end

# 抛出不同异常
begin
  begin
    raise "原始错误"
  rescue => e
    puts "捕获原始错误: #{e.message}"
    # 抛出新异常
    raise RuntimeError, "转换后的错误"
  end
  
rescue RuntimeError => e
  puts "捕获转换后错误: #{e.message}"
end

# 异常链接(Ruby 2.1+)
begin
  begin
    raise "原始错误"
  rescue => e
    raise "新错误", cause: e
  end
  
rescue => e
  puts "新错误: #{e.message}"
  puts "原始错误: #{e.cause.message}" if e.cause
end

🏗️ 自定义异常

创建自定义异常类

ruby
# 基本自定义异常
class CustomError < StandardError
end

# 带构造函数的自定义异常
class ValidationError < StandardError
  attr_reader :field, :value
  
  def initialize(field, value, message = nil)
    @field = field
    @value = value
    message ||= "字段 '#{field}' 的值 '#{value}' 无效"
    super(message)
  end
end

# 使用自定义异常
def validate_age(age)
  raise ValidationError.new("age", age) if age < 0 || age > 150
  true
end

begin
  validate_age(-5)
rescue ValidationError => e
  puts "验证错误: #{e.message}"
  puts "字段: #{e.field}, 值: #{e.value}"
end

# 层次化的自定义异常
class ApplicationError < StandardError
end

class DatabaseError < ApplicationError
  attr_reader :query
  
  def initialize(query, message = nil)
    @query = query
    message ||= "数据库查询失败: #{query}"
    super(message)
  end
end

class NetworkError < ApplicationError
  attr_reader :url
  
  def initialize(url, message = nil)
    @url = url
    message ||= "网络请求失败: #{url}"
    super(message)
  end
end

# 使用层次化异常
def fetch_data(url)
  # 模拟网络请求
  if url.include?("error")
    raise NetworkError.new(url, "连接超时")
  end
  "数据内容"
end

begin
  data = fetch_data("http://example.com/error")
rescue ApplicationError => e
  puts "应用错误: #{e.message}"
  case e
  when NetworkError
    puts "网络错误,URL: #{e.url}"
  when DatabaseError
    puts "数据库错误,查询: #{e.query}"
  end
end

🔧 异常相关方法

抛出异常

ruby
# 使用raise抛出异常
raise "这是一个错误消息"
raise RuntimeError, "运行时错误"
raise RuntimeError.new("自定义运行时错误")

# 使用fail抛出异常(与raise相同)
fail "这也是一个错误消息"
fail ArgumentError, "参数错误"

# 抛出特定异常对象
exception = ArgumentError.new("无效参数")
raise exception

# 重新抛出当前异常
begin
  raise "原始错误"
rescue
  puts "处理中..."
  raise  # 重新抛出
end

异常检查和处理

ruby
# 检查是否可以捕获异常
def safe_operation
  catch(:done) do
    # 可能抛出异常的操作
    throw :done, "操作完成"
  end
rescue => e
  "发生错误: #{e.message}"
end

# 使用retry重试
attempt = 0
begin
  attempt += 1
  puts "尝试第#{attempt}次"
  raise "模拟错误" if attempt < 3
  puts "成功了!"
  
rescue => e
  if attempt < 3
    puts "重试中..."
    retry
  else
    puts "最终失败: #{e.message}"
  end
end

异常信息获取

ruby
begin
  raise "自定义错误消息"
  
rescue => e
  puts "异常类: #{e.class}"
  puts "异常消息: #{e.message}"
  puts "异常追踪:"
  e.backtrace.each { |line| puts "  #{line}" }
  
  # 获取详细的异常信息
  puts "异常详情: #{e.inspect}"
  
  # 获取原因(Ruby 2.1+)
  puts "异常原因: #{e.cause}" if e.cause
end

🎯 实用异常处理示例

文件操作异常处理

ruby
class FileProcessor
  # 安全读取文件
  def self.safe_read(filename)
    File.open(filename, "r") do |file|
      file.read
    end
  rescue Errno::ENOENT
    puts "错误: 文件 '#{filename}' 不存在"
    nil
  rescue Errno::EACCES
    puts "错误: 没有权限读取文件 '#{filename}'"
    nil
  rescue => e
    puts "读取文件时发生未知错误: #{e.message}"
    nil
  end
  
  # 安全写入文件
  def self.safe_write(filename, content)
    File.open(filename, "w") do |file|
      file.write(content)
    end
    true
  rescue Errno::EACCES
    puts "错误: 没有权限写入文件 '#{filename}'"
    false
  rescue => e
    puts "写入文件时发生未知错误: #{e.message}"
    false
  end
  
  # 处理文件并返回结果
  def self.process_file(filename)
    content = safe_read(filename)
    return nil unless content
    
    # 处理内容
    processed_content = content.upcase
    
    # 写入新文件
    output_filename = "#{filename}.processed"
    safe_write(output_filename, processed_content)
    
    output_filename
  end
end

# 使用文件处理器
# result = FileProcessor.process_file("example.txt")
# if result
#   puts "文件处理完成: #{result}"
# else
#   puts "文件处理失败"
# end

网络请求异常处理

ruby
require 'net/http'
require 'uri'

class NetworkClient
  class NetworkError < StandardError
    attr_reader :url, :status_code
    
    def initialize(url, status_code = nil, message = nil)
      @url = url
      @status_code = status_code
      message ||= status_code ? 
        "HTTP #{status_code} 错误: #{url}" : 
        "网络请求失败: #{url}"
      super(message)
    end
  end
  
  class TimeoutError < NetworkError
    def initialize(url, message = nil)
      super(url, nil, message || "请求超时: #{url}")
    end
  end
  
  # 发送GET请求
  def self.get(url, timeout: 5)
    uri = URI(url)
    
    http = Net::HTTP.new(uri.host, uri.port)
    http.use_ssl = uri.scheme == 'https'
    http.open_timeout = timeout
    http.read_timeout = timeout
    
    response = http.get(uri.request_uri)
    
    case response
    when Net::HTTPSuccess
      response.body
    else
      raise NetworkError.new(url, response.code.to_i)
    end
    
  rescue Net::OpenTimeout, Net::ReadTimeout
    raise TimeoutError.new(url)
  rescue SocketError
    raise NetworkError.new(url, nil, "DNS解析失败: #{url}")
  rescue => e
    raise NetworkError.new(url, nil, "未知网络错误: #{e.message}")
  end
  
  # 安全的GET请求
  def self.safe_get(url, timeout: 5)
    get(url, timeout: timeout)
  rescue NetworkError => e
    puts "网络错误: #{e.message}"
    nil
  end
end

# 使用网络客户端
# response = NetworkClient.safe_get("https://api.github.com/users/octocat")
# if response
#   puts "响应内容: #{response[0..100]}..."
# else
#   puts "请求失败"
# end

数据验证异常处理

ruby
class DataValidator
  class ValidationError < StandardError
    attr_reader :field, :value, :constraint
    
    def initialize(field, value, constraint, message = nil)
      @field = field
      @value = value
      @constraint = constraint
      message ||= "字段 '#{field}' 的值 '#{value}' 不满足约束 '#{constraint}'"
      super(message)
    end
  end
  
  # 验证必填字段
  def self.validate_required(data, field)
    value = data[field]
    raise ValidationError.new(field, value, "required", 
            "字段 '#{field}' 是必填的") if value.nil? || value.to_s.empty?
    value
  end
  
  # 验证数字范围
  def self.validate_range(data, field, min, max)
    value = data[field]
    raise ValidationError.new(field, value, "range(#{min}-#{max})", 
            "字段 '#{field}' 必须在 #{min}#{max} 之间") unless value.is_a?(Numeric) && value.between?(min, max)
    value
  end
  
  # 验证邮箱格式
  def self.validate_email(data, field)
    value = data[field]
    email_pattern = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
    raise ValidationError.new(field, value, "email", 
            "字段 '#{field}' 必须是有效的邮箱地址") unless value.to_s.match?(email_pattern)
    value
  end
  
  # 批量验证
  def self.validate(data, rules)
    errors = []
    
    rules.each do |field, rule|
      begin
        case rule[:type]
        when :required
          validate_required(data, field)
        when :range
          validate_range(data, field, rule[:min], rule[:max])
        when :email
          validate_email(data, field)
        end
      rescue ValidationError => e
        errors << e
      end
    end
    
    unless errors.empty?
      raise ValidationError.new("multiple", nil, "multiple", 
              "验证失败,共 #{errors.length} 个错误")
    end
    
    true
  end
end

# 使用数据验证器
# user_data = {
#   name: "张三",
#   age: 25,
#   email: "zhangsan@example.com"
# }
# 
# validation_rules = {
#   name: { type: :required },
#   age: { type: :range, min: 0, max: 150 },
#   email: { type: :email }
# }
# 
# begin
#   DataValidator.validate(user_data, validation_rules)
#   puts "数据验证通过"
# rescue DataValidator::ValidationError => e
#   puts "验证错误: #{e.message}"
# end

📊 异常处理性能优化

异常处理性能考虑

ruby
# 避免使用异常控制正常流程
# 不推荐:使用异常控制流程
def find_user_bad(id)
  begin
    User.find(id)
  rescue ActiveRecord::RecordNotFound
    nil
  end
end

# 推荐:使用正常方法
def find_user_good(id)
  User.find_by(id: id)
end

# 预检查避免异常
def safe_divide(dividend, divisor)
  # 预检查
  return nil if divisor == 0
  
  dividend / divisor
end

# 使用rescue修饰符处理简单情况
def safe_parse_integer(string)
  Integer(string)
rescue ArgumentError
  nil
end

# 批量操作中的异常处理
def process_items(items)
  results = []
  errors = []
  
  items.each_with_index do |item, index|
    begin
      result = process_item(item)
      results << { index: index, item: item, result: result }
    rescue => e
      errors << { index: index, item: item, error: e.message }
    end
  end
  
  { results: results, errors: errors }
end

def process_item(item)
  # 处理单个项目
  item.upcase
end

异常日志记录

ruby
require 'logger'

class ExceptionLogger
  def initialize(log_file = "error.log")
    @logger = Logger.new(log_file)
    @logger.level = Logger::ERROR
  end
  
  # 记录异常
  def log_exception(e, context = {})
    error_info = {
      exception_class: e.class.name,
      message: e.message,
      backtrace: e.backtrace&.first(10),
      context: context,
      timestamp: Time.now
    }
    
    @logger.error(error_info.to_json)
  end
  
  # 带上下文的异常处理
  def handle_with_context(context = {})
    yield
  rescue => e
    log_exception(e, context)
    raise  # 重新抛出异常
  end
end

# 使用异常日志记录器
# logger = ExceptionLogger.new("app_errors.log")
# 
# logger.handle_with_context(user_id: 123, action: "update_profile") do
#   # 可能出错的操作
#   update_user_profile(user_id: 123, data: profile_data)
# end

🛡️ 异常处理最佳实践

1. 异常处理原则

ruby
# 原则1: 具体化异常处理
# 不好的做法
begin
  # 多种可能的错误
  file = File.open("data.txt")
  data = JSON.parse(file.read)
  result = 10 / data["divisor"]
rescue => e
  puts "出错了: #{e.message}"
end

# 好的做法
begin
  file = File.open("data.txt")
  data = JSON.parse(file.read)
  result = 10 / data["divisor"]
rescue Errno::ENOENT => e
  puts "文件不存在: #{e.message}"
rescue JSON::ParserError => e
  puts "JSON解析错误: #{e.message}"
rescue ZeroDivisionError => e
  puts "除零错误: #{e.message}"
rescue => e
  puts "未知错误: #{e.message}"
end

# 原则2: 不要忽略异常
# 不好的做法
begin
  risky_operation
rescue
  # 什么都不做
end

# 好的做法
begin
  risky_operation
rescue => e
  log_error(e)
  # 或者重新抛出
  raise
end

# 原则3: 清理资源
# 好的做法
file = nil
begin
  file = File.open("data.txt")
  process_file(file)
ensure
  file&.close  # 确保文件被关闭
end

# 更好的做法
File.open("data.txt") do |file|
  process_file(file)
end  # 自动关闭文件

2. 自定义异常设计

ruby
# 设计良好的异常层次结构
module MyApp
  class Error < StandardError
    attr_reader :context
    
    def initialize(message = nil, context = {})
      @context = context
      super(message)
    end
  end
  
  class ValidationError < Error
    attr_reader :field, :value
    
    def initialize(field, value, message = nil, context = {})
      @field = field
      @value = value
      message ||= "字段 '#{field}' 验证失败"
      super(message, context)
    end
  end
  
  class BusinessLogicError < Error
    attr_reader :code
    
    def initialize(code, message = nil, context = {})
      @code = code
      message ||= "业务逻辑错误: #{code}"
      super(message, context)
    end
  end
  
  class ExternalServiceError < Error
    attr_reader :service, :http_status
    
    def initialize(service, http_status = nil, message = nil, context = {})
      @service = service
      @http_status = http_status
      message ||= http_status ? 
        "#{service} 服务错误 (HTTP #{http_status})" : 
        "#{service} 服务错误"
      super(message, context)
    end
  end
end

# 使用自定义异常
begin
  raise MyApp::ValidationError.new("email", "invalid-email", 
          "邮箱格式不正确", { user_id: 123 })
rescue MyApp::ValidationError => e
  puts "验证错误: #{e.message}"
  puts "字段: #{e.field}, 值: #{e.value}"
  puts "用户ID: #{e.context[:user_id]}"
end

3. 异常处理模式

ruby
# 重试模式
class RetryableOperation
  def self.execute(max_retries: 3, base_delay: 1)
    retries = 0
    
    begin
      yield
    rescue => e
      retries += 1
      
      if retries <= max_retries
        delay = base_delay * (2 ** (retries - 1))  # 指数退避
        puts "第#{retries}次重试,等待#{delay}秒..."
        sleep(delay)
        retry
      else
        puts "重试#{max_retries}次后仍然失败: #{e.message}"
        raise
      end
    end
  end
end

# 使用重试模式
# RetryableOperation.execute(max_retries: 3) do
#   # 可能失败的操作
#   unstable_network_call
# end

# 电路断路器模式
class CircuitBreaker
  def initialize(threshold: 5, timeout: 60)
    @threshold = threshold
    @timeout = timeout
    @failure_count = 0
    @last_failure_time = nil
    @open = false
  end
  
  def call
    return yield if closed?
    
    if timeout_expired?
      @open = false
      @failure_count = 0
      return yield
    end
    
    raise "电路断路器已打开"
  end
  
  def record_failure
    @failure_count += 1
    @last_failure_time = Time.now
    @open = @failure_count >= @threshold
  end
  
  private
  
  def closed?
    !@open
  end
  
  def timeout_expired?
    @last_failure_time && (Time.now - @last_failure_time) > @timeout
  end
end

# 使用电路断路器
# breaker = CircuitBreaker.new(threshold: 3, timeout: 30)
# 
# begin
#   breaker.call do
#     external_service_call
#   end
# rescue => e
#   breaker.record_failure
#   raise
# end

4. 实际应用示例

ruby
# Web应用控制器异常处理
class ApplicationController
  def handle_request
    begin
      # 处理请求
      process_request
      
    rescue ValidationError => e
      render json: { error: "验证错误", details: e.message }, status: 400
      
    rescue RecordNotFound => e
      render json: { error: "资源未找到", details: e.message }, status: 404
      
    rescue BusinessLogicError => e
      render json: { error: "业务错误", code: e.code, details: e.message }, status: 422
      
    rescue ExternalServiceError => e
      Rails.logger.error "外部服务错误: #{e.message}"
      render json: { error: "服务暂时不可用" }, status: 503
      
    rescue => e
      Rails.logger.error "未处理的异常: #{e.class} - #{e.message}"
      Rails.logger.error e.backtrace.join("\n")
      render json: { error: "内部服务器错误" }, status: 500
    end
  end
  
  private
  
  def process_request
    # 实际的请求处理逻辑
  end
end

# 数据库操作异常处理
class DatabaseManager
  def self.with_transaction
    ActiveRecord::Base.transaction do
      yield
    end
  rescue ActiveRecord::RecordInvalid => e
    Rails.logger.error "记录验证失败: #{e.message}"
    raise ValidationError.new("database", nil, "validation", e.message)
  rescue ActiveRecord::RecordNotFound => e
    Rails.logger.error "记录未找到: #{e.message}"
    raise RecordNotFound.new(e.message)
  rescue ActiveRecord::StatementInvalid => e
    Rails.logger.error "SQL语句错误: #{e.message}"
    raise BusinessLogicError.new("DB001", "数据库操作失败")
  end
end

📚 下一步学习

掌握了Ruby异常处理后,建议继续学习:

继续您的Ruby学习之旅吧!

本站内容仅供学习和研究使用。