Claude引路星,带你驾驭AI对话新境界

错误与异常处理 实用技巧

所属主题: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 的 winstonpino。拒绝依赖 printconsole.log,它们在生产环境中既无法控制输出级别,也难以集中管理。
  • 外部服务超时配置:如果你的代码调用 API、数据库或文件系统,务必确认这些客户端的超时参数已设置。例如,Python 中 requests.timeoutmysql.connector.connect(timeout=...),TypeScript 中 AbortControllersetTimeout未设置超时的异常处理,在慢调用场景下形同虚设

最常见的心理准备缺失

许多开发者犯下的第一个错误,并非 catch 块写得不对,而是在开始处理异常之前,从未意识到调用可能失败。典型表现包括:

  • 不检查文件是否成功打开,就直接读取内容;
  • 不验证 API 返回的 JSON 结构是否符合预期,就盲目将 response["data"] 传给下一个函数;
  • 认为只有网络错误才算异常,完全忽略磁盘满、权限变更、进程被杀死等非典型情况。

警示信号:如果你发现自己正在写 except Exception: pass,请先停下来,重新审视上述前提。


核心步骤:构建结构化异常处理流程

以下流程适用于大多数后端服务或脚本,按执行顺序排列。

1. 划分“可预期”与“不可预期”错误

可预期错误:你知道特定输入或环境会导致某个已知错误,例如除零、键缺失、网络超时。
不可预期错误:你无法预知具体原因,例如第三方库内部段错误、内核 OOM 杀进程。

处理原则

  • 对于可预期错误,使用显式条件检查提前拦截,尽量在错误发生之前避免。例如,使用 if divisor == 0 而非 try: result = a / b
  • 对于不可预期错误,配置统一的全局兜底机制(如 finallyuncaughtExceptionHandler),记录现场并优雅退出。

实际示例(读取配置文件):

# 可预期:文件不存在 → 使用默认值
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  # 重新抛出,交由外层或进程管理器处理

关键区别:不要将 FileNotFoundErrorPermissionError 合并为一个 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 的组合。缺少 finallytry 在异常发生时会导致文件句柄或网络连接泄露。
  • 是否存在 except: pass 且没有日志?如果是,立即移除或至少添加一行日志记录。
  • 是否测试过“模拟失败”场景?编写一个小脚本,通过 mockpatch 模拟抛异常的函数,验证 catch 层是否正确处理。
  • 日志中是否记录了异常栈(exc_info=True%s % e.__traceback__)?仅记录消息而不记录栈,在线上环境中等同于没有日志。
  • 超时参数是否已硬编码或配置化,而非依赖默认的无穷大?检查 requests.getsubprocess.runsocket.settimeout 等关键调用。

边界情况:什么时候选择退出而非恢复

  • 当异常源自“资源完全耗尽”(如磁盘满、内存不足、文件描述符用尽)时,不要在异常处理块中试图恢复。例如,磁盘满时写日志只会加剧问题。应记录最精简的标识(如进程 ID 和异常类型)到系统日志(syslog),然后退出。
  • 如果代码运行在容器化环境中,并且 Kubernetes/Terraform 会自动重启容器,抛出致命异常(如 sys.exit(1))往往比捕获所有错误更优。K8s 的重启策略可以自动处理这些情况,保持服务的高可用性。

故障排除:常见问题的根源与解法

场景 1:异常被吞没,用户无反馈

检查点:确认日志级别。许多新手在开发环境中使用 logger.debug(),而生产环境日志级别设为 WARNING 以上,导致 debug 消息永不出现。标准做法:将重要异常记录为 logger.warninglogger.error

场景 2:catch 了异常但程序仍崩溃

检查点:是否在 catch 块内又抛出了未 catch 的异常?或者 catch 的异常类型不匹配(例如只 catch KeyError,但实际抛出 IndexError)?最佳实践:编写单元测试,直接