错误与异常处理 实用技巧
所属主题:Claude 提示词工程完全指南
本文聚焦「错误与异常处理 实用技巧」。这一节不是泛泛介绍,而是把「错误与异常处理 实用技巧」放到「错误与异常处理」分类下,说明适用前提、操作边界、检查方法和容易忽略的风险点。
差异化实操边界:示例会围绕 Claude、API、SDK、MCP、上下文、权限、日志和成本控制等实际接入场景展开,强调配置边界、排错顺序和上线前检查。 你可以先核对当前环境、权限、版本和目标结果,再决定是否继续执行。
错误与异常处理实用技巧
核心原则:从防御性编程到优雅回滚
错误与异常处理绝非简单的 try/catch 包装,也不是在每个操作后机械地加上 if error。一套真正可靠的异常处理机制,需要回答三个核心问题:程序可能在哪个环节失败?失败后用户将看到什么?如何确保数据不损坏?本文围绕这三个问题,提供从防御性检查到回滚策略的全套可落地方案,涵盖 Python 与 TypeScript 的典型场景,并揭示新手最容易忽略的边界陷阱。
前置准备:奠定可靠的基石
硬性条件确认清单
- 语言版本:Python 3.7+ 或 TypeScript 4.x+。Python 3.6 及更早版本缺少异常链语法支持;TypeScript 4.0 以下无法使用
unknown替代any作为 catch 类型,这会削弱类型安全。 - 日志框架就绪:至少配置一个支持时间戳和日志级别的工具,例如 Python 的
logging模块,或 TypeScript 的winston、pino。拒绝依赖print或console.log,它们在生产环境中既无法控制输出级别,也难以集中管理。 - 外部服务超时配置:如果你的代码调用 API、数据库或文件系统,务必确认这些客户端的超时参数已设置。例如,Python 中
requests.timeout或mysql.connector.connect(timeout=...),TypeScript 中AbortController或setTimeout。未设置超时的异常处理,在慢调用场景下形同虚设。
最常见的心理准备缺失
许多开发者犯下的第一个错误,并非 catch 块写得不对,而是在开始处理异常之前,从未意识到调用可能失败。典型表现包括:
- 不检查文件是否成功打开,就直接读取内容;
- 不验证 API 返回的 JSON 结构是否符合预期,就盲目将
response["data"]传给下一个函数; - 认为只有网络错误才算异常,完全忽略磁盘满、权限变更、进程被杀死等非典型情况。
警示信号:如果你发现自己正在写 except Exception: pass,请先停下来,重新审视上述前提。
核心步骤:构建结构化异常处理流程
以下流程适用于大多数后端服务或脚本,按执行顺序排列。
1. 划分“可预期”与“不可预期”错误
可预期错误:你知道特定输入或环境会导致某个已知错误,例如除零、键缺失、网络超时。
不可预期错误:你无法预知具体原因,例如第三方库内部段错误、内核 OOM 杀进程。
处理原则:
- 对于可预期错误,使用显式条件检查提前拦截,尽量在错误发生之前避免。例如,使用
if divisor == 0而非try: result = a / b。 - 对于不可预期错误,配置统一的全局兜底机制(如
finally或uncaughtExceptionHandler),记录现场并优雅退出。
实际示例(读取配置文件):
# 可预期:文件不存在 → 使用默认值
try:
with open("config.toml") as f:
cfg = parse_toml(f.read())
except FileNotFoundError:
cfg = DEFAULT_CONFIG # 允许继续运行,但记录日志
except PermissionError:
raise RuntimeError("无权限读取配置文件,请检查文件所有者")
except Exception as e:
# 不可预期:解析器抛出的未知错误
logger.critical("读取配置时出现未知错误: %s", e, exc_info=True)
raise # 重新抛出,交由外层或进程管理器处理
关键区别:不要将 FileNotFoundError 和 PermissionError 合并为一个 except OSError as e 进行相同处理。两种错误指向不同的用户操作:一个需要检查路径,另一个需要检查权限。
2. 设计“失败时数据不损坏”的回滚机制
当操作跨多个步骤且改变外部状态(如写文件、改数据库、发消息)时,必须设计回滚或补偿策略。
核心模式:为每个会改变状态的操作注册一个撤销函数,并将其放置在 finally 块中。
backup = []
try:
for item in items:
result = write_to_database(item)
backup.append(item) # 记录已写入的条目
except DatabaseError as e:
for item in reversed(backup): # 逆序撤销
undo_write(item)
raise
关键细节:撤销顺序必须逆序。如果先写入 A 再写入 B,回滚时必须先删 B 再删 A,否则可能违反外键约束或数据一致性。
3. 选择合适的异常粒度:从宽泛到精准
| 异常范围 | 适用场景 | 潜在风险 |
|---|---|---|
except ValueError |
明确的业务数据校验错误 | 不会误捕其他异常 |
except (OSError, ConnectionError) |
文件或网络操作 | 可能遗漏 TimeoutError |
except Exception |
顶层兜底,仅用于日志与优雅退出 | 会捕获 KeyboardInterrupt,需再次 raise |
except BaseException |
几乎不用 | 会捕获 SystemExit,导致隔离失效 |
一个被低估的设计模式:为项目定义自定义异常层次。
class AppError(Exception):
"""应用中所有自定义异常的基类"""
pass
class ConfigError(AppError):
"""配置相关错误"""
pass
class APIError(AppError):
"""第三方接口错误,包含 HTTP 状态码和原始响应"""
def __init__(self, status_code: int, response_body: str):
super().__init__(f"API returned {status_code}: {response_body[:200]}")
self.status_code = status_code
self.response_body = response_body
这样,外层代码可以借助 except AppError 区分“业务异常”与“语言运行时异常”,提高代码的可读性和可维护性。
4. 给异常添加上下文:异常链的威力
Python 3 的 raise ... from ... 和 TypeScript 的 cause 属性能够保留原始异常栈。避免以下写法:
try:
process(user_data)
except KeyError as e:
raise ValueError("用户数据缺少必要字段")
这会丢失 KeyError 的原始栈,调试时无法追踪缺失的具体字段。正确做法:
except KeyError as e:
raise ValueError("用户数据缺少必要字段") from e
TypeScript 等效写法:
try {
process(userData);
} catch (e: unknown) {
throw new ValueError("用户数据缺少必要字段", { cause: e });
}
验证检查:确保代码真正可靠
完成异常处理后,使用以下清单进行自我校验:
检查清单
- 每个
try是否都有对应的finally?至少是except Exception+finally的组合。缺少finally的try在异常发生时会导致文件句柄或网络连接泄露。 - 是否存在
except: pass且没有日志?如果是,立即移除或至少添加一行日志记录。 - 是否测试过“模拟失败”场景?编写一个小脚本,通过
mock或patch模拟抛异常的函数,验证 catch 层是否正确处理。 - 日志中是否记录了异常栈(
exc_info=True或%s % e.__traceback__)?仅记录消息而不记录栈,在线上环境中等同于没有日志。 - 超时参数是否已硬编码或配置化,而非依赖默认的无穷大?检查
requests.get、subprocess.run、socket.settimeout等关键调用。
边界情况:什么时候选择退出而非恢复
- 当异常源自“资源完全耗尽”(如磁盘满、内存不足、文件描述符用尽)时,不要在异常处理块中试图恢复。例如,磁盘满时写日志只会加剧问题。应记录最精简的标识(如进程 ID 和异常类型)到系统日志(syslog),然后退出。
- 如果代码运行在容器化环境中,并且 Kubernetes/Terraform 会自动重启容器,抛出致命异常(如
sys.exit(1))往往比捕获所有错误更优。K8s 的重启策略可以自动处理这些情况,保持服务的高可用性。
故障排除:常见问题的根源与解法
场景 1:异常被吞没,用户无反馈
检查点:确认日志级别。许多新手在开发环境中使用 logger.debug(),而生产环境日志级别设为 WARNING 以上,导致 debug 消息永不出现。标准做法:将重要异常记录为 logger.warning 或 logger.error。
场景 2:catch 了异常但程序仍崩溃
检查点:是否在 catch 块内又抛出了未 catch 的异常?或者 catch 的异常类型不匹配(例如只 catch KeyError,但实际抛出 IndexError)?最佳实践:编写单元测试,直接