第10章 错误处理与异常
区分正常absence与异常失败,掌握begin/rescue/else/ensure、限定捕获、异常类、raise与有界retry,保留cause和cleanup证据。
学习目标
- 能解释错误处理与begin、rescue与else在“第10章 错误处理与异常”中的责任边界
- 能围绕“怎样划分异常的产生、传播、恢复与 ensure 清理责任?”运行正常与故障轨迹并定位首个分岔
- 能用“第10章 错误处理与异常的输入样本、接收者与方法、关键状态前后值、正常与失败输出、异常或退出状态,以及复位后的再次运行记录。”证明“只捕获当前层能够恢复的异常,资源清理覆盖成功和失败路径。”
来源、版次与运行边界
“第10章 错误处理与异常”以作者维护的第 5 版支持页核定 2016 年 3 月 12 日首刷、两位作者、松本行弘监修以及四部分 23 章目录;逐章程序清单、练习答案和勘误只作为公开支持材料,不被冒充为原书全文。
对“第10章 错误处理与异常”而言,中文解释、示例、交互、练习和答案均为独立教学重写;站内中文章名是与官方 23 章顺序对应的课程映射,不宣称是日文小节的逐字翻译,也不从公开程序清单复制整段实现。
“第10章 错误处理与异常”固定在 Ruby 2.3 语境;Ruby 2.3.0 官方文档与稳定版发布说明用于核对当时可用的语言和标准库行为。现代 Ruby 的差异只能另列迁移说明,不能静默改变本页示例的版本结论。
围绕“怎样划分异常的产生、传播、恢复与 ensure 清理责任?”,本页验收“第10章 错误处理与异常的输入样本、接收者与方法、关键状态前后值、正常与失败输出、异常或退出状态,以及复位后的再次运行记录。”。先预测正常轨迹,再只注入“捕获过宽异常并返回成功值,掩盖状态已经不可信”;若无法定位第一处状态分岔,就拒绝当前解释。
从“所有失败都rescue”这个反模式开始
异常不是把所有不顺利情况统一成一个false。Search无结果可能是正常domain value,参数格式错误需要caller修复,网络timeout可能重试,不变量破坏则要停止边界并保存证据。先于rescue syntax。
先预测:rescue StandardError; nil会带来什么?它把NoMethodError、配置错误、磁盘失败和合法missing都压成nil,caller无法决定重试、告警还是显示空结果。异常处理的第一目标不是“程序继续跑”,而是让正确owner作出有证据的决定。
关于错误处理
Method contract应说明成功shape、正常absence、会抛哪些domain/standard exceptions、是否可重试、失败后state是否改变。防止每层重复log/rescue。
底层I/O知道path和system error,中层知道业务operation,CLI知道stderr与exit status。每层可添加context并保留cause,但只在能恢复、转成稳定domain error或建立process boundary时rescue。不要把programming error伪装成空数据。
异常处理:begin、rescue与else
begin包围可能失败的最小operation,rescue按exception class匹配,else只在begin body无异常时执行。是正确性关键。
def load_integer(path)
text = File.read(path)
rescue Errno::ENOENT => error
raise ConfigError, "config missing: #{path}", error.backtrace
else
Integer(text.strip, 10)
end上例rescue只围绕File.read;Integer conversion放else,因此其ArgumentError不会被ENOENT rescue影响。Ruby 2.3构造cause的API细节与现代Ruby不同,直接在rescue中raise新异常会自动形成cause链;若手工backtrace需谨慎。更常见写法是在一个明确boundary保留raise ConfigError, message并让exception.cause指向原错误。
多个rescue从具体到宽泛排列。Bare rescue默认捕获StandardError及subclasses,不捕获Exception下的全部严重控制异常;不要rescue Exception吞掉Interrupt、SystemExit等process signals。
异常处理的写法
Method body可直接带rescue/else/ensure,等价于隐式begin,适合整个method就是一个窄operation;复杂method使用显式begin缩小范围。Rescue variable保存exception,可读取class、message、backtrace和cause。
def parse_record(line)
JSON.parse(line)
rescue JSON::ParserError => error
raise InvalidRecord, "invalid JSON record", error.backtrace
end错误message应带operation identity、非敏感input locator和期望,不要把token/password/完整payload写日志。Exception对象可以携带structured fields,避免caller解析message字符串。自定义异常在本章后面说明。
ensure后处理
Ensure无论成功、异常、return/break等control exit都会运行,适合close、unlock、restore state与删除temp resource。要求ensure尽量简单可靠。
lock.lock
begin
update_shared_state
ensure
lock.unlock
end优先使用提供block lifecycle的API,如File.open {}、Mutex#synchronize;它们封装setup/ensure。Ensure中return会覆盖method正常返回和异常,几乎总是错误;ensure再次raise也可能mask原异常。Cleanup失败若必须记录,保留primary exception并在boundary报告secondary evidence。
retry重试
在rescue中retry重新执行对应begin body。它必须有attempt上限或deadline、只捕获transient class、backoff/jitter、可取消、observability和idempotency策略。防止无限循环与重复写。
attempt = 0
begin
attempt += 1
client.fetch(key)
rescue Timeout::Error => error
raise if attempt >= 3
sleep(0.05 * (2 ** (attempt - 1)))
retry
end该示例为教学简化:production sleep注入clock/sleeper以便测试,增加jitter,尊重overall deadline。POST/charge等side effect在不确定响应后重试可能重复执行,需要idempotency key或transaction protocol。Invalid input、authentication failure和programming error不重试。
rescue修饰符
expression rescue fallback写法紧凑,却容易捕获StandardError并隐藏哪一步失败,precedence也常被误读。只在非常局部、fallback安全且错误证据确实不需要的probe使用;配置、I/O、解析和业务操作优先完整rescue并限定class。
例如Integer(text) rescue nil会把任何StandardError压成nil;更清楚写Integer(text, 10)并只rescue ArgumentError,或者使用明确parser method返回Result。极窄。
异常处理语法的补充与指定需要捕捉的异常
一个rescue可列多个相关classes:rescue IOError, SystemCallError => error,但组合前确认恢复策略相同。Class matching使用inheritance,因此先写specific rescue;最后宽泛StandardError仅在process/job boundary统一报告与转换status。
Re-raise当前异常写裸raise,保留class/message/backtrace;raise error在某些情形会改变backtrace起点,目标版本需验证。添加context时raise domain error并保留cause。不要同一异常每层都log,通常最终处理owner记录一次,intermediate layer只augment。
Rescue中访问外部服务、执行复杂template或序列化任意对象可能再次失败。Error path也要有测试和资源预算;最低可靠报告可退化为class、safe message、operation id和backtrace locator。
异常类
自定义业务异常通常继承StandardError,而不是Exception。建立小而有意义的hierarchy,如ConfigError < StandardError、InvalidRecord < StandardError、TransientFetchError < StandardError;caller按可恢复policy捕获,不按每个底层library class散布逻辑。
让依赖可替换。Exception可添加只读structured fields,如path、record_index、attempt,但避免携带巨大/敏感payload。
Equality、serialization与cross-process传输不是Exception默认承诺;跨job boundary应转成明确error record。测试自定义异常的class、fields、cause和message,不依赖完整backtrace文本。
主动抛出异常
raise ArgumentError, "..."用于caller违反参数contract;raise StateError用于object不允许的transition;bare raise在rescue中重新抛当前exception。避免用异常做普通循环控制。
Guard clause在副作用前验证并raise,使失败保持state不变。若部分副作用已发生,需要transaction/compensation并把partial state写进error context。Library不要随意exit;CLI adapter把domain exceptions映射成stderr与status,process boundary才决定退出。
正式节点与章专属证据
- ↡在“第10章 错误处理与异常”中,错误处理必须连接输入、状态变化与可复核结果。 :第 1 个正式节点要能回到“只捕获当前层能够恢复的异常,资源清理覆盖成功和失败路径。”,并说明故障发生前后的第一处差异。
- ↡在“第10章 错误处理与异常”中,begin、rescue与else必须连接输入、状态变化与可复核结果。 :第 2 个正式节点要能回到“只捕获当前层能够恢复的异常,资源清理覆盖成功和失败路径。”,并说明故障发生前后的第一处差异。
- ↡在“第10章 错误处理与异常”中,ensure后处理必须连接输入、状态变化与可复核结果。 :第 3 个正式节点要能回到“只捕获当前层能够恢复的异常,资源清理覆盖成功和失败路径。”,并说明故障发生前后的第一处差异。
- ↡在“第10章 错误处理与异常”中,retry重试必须连接输入、状态变化与可复核结果。 :第 4 个正式节点要能回到“只捕获当前层能够恢复的异常,资源清理覆盖成功和失败路径。”,并说明故障发生前后的第一处差异。
- ↡在“第10章 错误处理与异常”中,rescue修饰符必须连接输入、状态变化与可复核结果。 :第 5 个正式节点要能回到“只捕获当前层能够恢复的异常,资源清理覆盖成功和失败路径。”,并说明故障发生前后的第一处差异。
- ↡在“第10章 错误处理与异常”中,指定需要捕捉的异常必须连接输入、状态变化与可复核结果。 :第 6 个正式节点要能回到“只捕获当前层能够恢复的异常,资源清理覆盖成功和失败路径。”,并说明故障发生前后的第一处差异。
- ↡在“第10章 错误处理与异常”中,异常类必须连接输入、状态变化与可复核结果。 :第 7 个正式节点要能回到“只捕获当前层能够恢复的异常,资源清理覆盖成功和失败路径。”,并说明故障发生前后的第一处差异。
- ↡在“第10章 错误处理与异常”中,主动抛出异常必须连接输入、状态变化与可复核结果。 :第 8 个正式节点要能回到“只捕获当前层能够恢复的异常,资源清理覆盖成功和失败路径。”,并说明故障发生前后的第一处差异。
对象与状态模型
从输入、接收者到可观察证据
怎样划分异常的产生、传播、恢复与 ensure 清理责任?
输入与接收者
固定错误处理所需的原始值、Ruby 版本和调用入口。
状态变化
在执行前记录接收者身份,并声明begin、rescue与else的允许状态。
观察证据
保存第10章 错误处理与异常的初值、参数、编码或资源位置。
正式节点:错误处理、begin、rescue与else、ensure后处理、retry重试、rescue修饰符、指定需要捕捉的异常、异常类、主动抛出异常
控制与消息轨迹
在相同初值下定位首个分岔
- 01固定错误处理的输入和接收者
- 02执行begin、rescue与else并记录状态
- 03观察ensure后处理的返回或副作用
- 04用主动抛出异常核对不变量并复位
运行不变量:只捕获当前层能够恢复的异常,资源清理覆盖成功和失败路径。
边界故障探针
一次只破坏一个前提
本章回顾:只恢复真正拥有的失败
- 先区分正常absence、invalid input、transient failure与invariant violation,再选择value或exception。
- Rescue region保持窄,else承载success-only work,ensure负责不遮蔽原结果的cleanup。
- 只捕获能处理的specific classes;re-raise保留backtrace/cause,domain error添加安全context。
- Retry需要transient、bounded、backoff、cancel与idempotency;rescue modifier只用于极窄fallback。
- Exception hierarchy按caller recovery action设计,raise表示当前method无法履行contract。
练习与答案
练习
- 问题 1:建立正常轨迹。 回答“怎样划分异常的产生、传播、恢复与 ensure 清理责任?”,并写出四步执行记录。
- 问题 2:注入单一故障。 只制造“捕获过宽异常并返回成功值,掩盖状态已经不可信”,应从哪里开始定位?
- 问题 3:覆盖正式节点。 用一个证据包串联错误处理、begin、rescue与else、ensure后处理、retry重试、rescue修饰符、指定需要捕捉的异常、异常类、主动抛出异常,说明为什么结论可由另一位读者独立复核。
名词解释
名词解释
本章出现的专业名词,用大白话再讲一遍。
- 错误处理
“第10章 错误处理与异常”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。
- begin、rescue与else
“第10章 错误处理与异常”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。
- ensure后处理
“第10章 错误处理与异常”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。
- retry重试
“第10章 错误处理与异常”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。
- rescue修饰符
“第10章 错误处理与异常”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。
- 指定需要捕捉的异常
“第10章 错误处理与异常”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。
- 异常类
“第10章 错误处理与异常”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。
- 主动抛出异常
“第10章 错误处理与异常”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。