skills/exception-best-practices/SKILL.md
异常处理与创建最佳实践指南(语言无关)。涵盖 try/catch/finally 使用、常见条件预检、异步异常处理、状态恢复、异常重抛、自定义异常类型设计等全场景指导。当用户在项目中需要设计、优化或审查异常处理策略时使用此技能。
npx skillsauth add cruldra/skills exception-best-practicesInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
正确的异常处理对应用程序的可靠性至关重要。你可以有意识地处理预期的异常来防止应用崩溃。然而,一个崩溃的应用比一个行为未定义的应用更可靠、更易于诊断。
本技能提供语言无关的异常处理和创建指导,从处理策略到自定义异常设计,覆盖实战中的各种场景。
| 实践 | 影响 | 难度 | |------|------|------| | 使用 try/catch/finally 恢复错误或释放资源 | ⭐⭐⭐⭐⭐ | ⭐ | | 处理常见条件以避免异常 | ⭐⭐⭐⭐ | ⭐⭐ | | 优先使用不抛异常的替代 API | ⭐⭐⭐⭐ | ⭐ | | 正确处理取消和异步异常 | ⭐⭐⭐⭐ | ⭐⭐⭐ | | 设计可避免异常的类 | ⭐⭐⭐ | ⭐⭐⭐ | | 异常时恢复状态 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | | 正确捕获和重抛异常 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | | 使用预定义异常类型 | ⭐⭐⭐⭐ | ⭐ | | 使用异常构建器方法 | ⭐⭐⭐ | ⭐⭐ | | 自定义异常遵循命名约定 | ⭐⭐⭐ | ⭐ | | 自定义异常提供标准构造方式 | ⭐⭐⭐ | ⭐⭐ |
对可能生成异常的代码,且应用可以从该异常中恢复时,使用 try/except 块包围代码。
关键规则:
except 块中,始终从最具体的异常到最通用的异常排序——通用的 except 不会处理更具体的异常with、Java 的 try-with-resources、C# 的 using、Go 的 defer)finally 块清理无法自动管理的资源——finally 中的代码即使抛出异常也几乎总是会执行# ✅ 异常从最具体到最通用排序
try:
result = process_data(data)
except ValidationError: # 最具体的异常优先
handle_validation()
except ProcessingError: # 然后是更通用的
handle_processing()
finally:
cleanup_resources()
# ✅ 使用 with 自动管理资源
with open("file.txt") as f:
content = f.read()
对于可能发生但会触发异常的条件,考虑以避免异常的方式处理。
# ✅ 先检查状态,避免异常
if conn.is_connected():
conn.close()
# ❌ 不检查直接捕获异常
try:
conn.close()
except ConnectionError as e:
print(e)
选择策略:
| 场景 | 推荐方式 | 原因 | |------|----------|------| | 事件很少发生(真正的异常情况) | 异常处理 | 正常条件下执行的代码更少 | | 事件经常发生(正常执行的一部分) | 预先检查条件 | 避免异常开销,执行代码更少 |
注意:预先检查在大多数情况下能消除异常,但在竞态条件下,被保护的条件可能在检查和操作之间发生变化,此时仍可能产生异常。
当异常的性能代价过高时,优先使用返回错误状态而非抛出异常的 API。
# ❌ 无效输入时抛出 ValueError
value = int(user_input)
# ✅ LBYL(Look Before You Leap)先验证再操作
if user_input.isdigit():
value = int(user_input)
# ✅ 使用 dict.get() 代替 dict[key],不抛 KeyError
result = my_dict.get(key, default_value)
# ✅ 使用 getattr 提供默认值,不抛 AttributeError
value = getattr(obj, "attr_name", default_value)
import asyncio
# ✅ 捕获 CancelledError,清理后重新传播
try:
await some_async_operation()
except asyncio.CancelledError:
cleanup()
raise # 不要吞掉取消异常
# ✅ 捕获 Task 中存储的异常
task = asyncio.create_task(some_coroutine())
try:
result = await task # 异常在这里抛出
except SomeError as e:
handle_error(e)
类可以提供方法或属性,让调用者避免触发异常。
# ✅ 提供检查方法,让调用者避免异常
class DataReader:
def has_next(self) -> bool:
"""调用者可先检查是否有下一条数据"""
return self._position < len(self._data)
def read_next(self):
if not self.has_next():
raise StopIteration("No more data")
item = self._data[self._position]
self._position += 1
return item
# 调用者可以避免异常
while reader.has_next():
item = reader.read_next()
通用策略:
None 而非抛出异常,将其视为正常控制流has_next()、exists()、is_valid())让调用者预先检查调用者应假设方法抛出异常时不会产生副作用。
# ❌ 如果存款失败,取款不应保留生效
def transfer_funds(from_acct, to_acct, amount):
from_acct.withdraw(amount)
to_acct.deposit(amount) # 如果这里失败,取款已经生效了
# ✅ 捕获异常并回滚
def transfer_funds(from_acct, to_acct, amount):
withdrawal_id = from_acct.withdraw(amount)
try:
to_acct.deposit(amount)
except Exception:
from_acct.rollback(withdrawal_id)
raise # 重抛原始异常
替代方案:抛出新异常并将原始异常作为 cause:
except Exception as e:
from_acct.rollback(withdrawal_id)
raise TransferError("Deposit failed after withdrawal") from e
异常携带的堆栈追踪从抛出异常的方法开始,到捕获异常的方法结束。错误的重抛方式会丢失原始堆栈信息。
# ✅ 不带参数的 raise 保留原始堆栈
try:
do_something()
except SomeError:
log_error()
raise # 保留原始堆栈追踪
# ✅ 使用 raise ... from 链接异常,保留因果链
try:
do_something()
except SomeError as e:
raise NewError("Context info") from e
# ❌ 重新创建异常,丢失原始堆栈
try:
do_something()
except SomeError as e:
raise SomeError(str(e)) # 堆栈追踪从这里重新开始!
仅当标准类型不适用时才引入新的异常类。常用标准异常类型:
| 场景 | Python 异常类型 |
|------|----------------|
| 无效参数 | ValueError |
| 参数类型错误 | TypeError |
| 对象状态不正确 | RuntimeError |
| 未实现的功能 | NotImplementedError |
| 索引越界 | IndexError |
| 键不存在 | KeyError |
| 属性不存在 | AttributeError |
| IO 错误 | OSError / IOError |
| 超时 | TimeoutError |
| 权限不足 | PermissionError |
| 文件未找到 | FileNotFoundError |
当类在多个地方抛出相同异常时,创建辅助方法来构建异常,减少重复代码:
class FileReader:
def __init__(self, path: str):
self._path = path
def read(self, size: int) -> bytes:
data = self._read_from_file(size)
if data is None:
raise self._new_io_error()
return data
def read_line(self) -> str:
line = self._read_line_from_file()
if line is None:
raise self._new_io_error()
return line
# 异常构建器方法——多处复用
def _new_io_error(self) -> FileReaderError:
return FileReaderError(f"Failed to read file: {self._path}")
# ✅ 清晰、有上下文
raise ValueError(f"Order amount must be positive, got {amount}.")
raise ConnectionError(f"Failed to connect to {host}:{port} after {retries} retries.")
# ❌ 模糊、缺少上下文
raise ValueError("Invalid value")
raise ConnectionError("Connection failed")
finally 块用于释放资源,不应引入新的异常。如果清理操作可能失败,应捕获并记录,而非让其传播:
# ✅ 清理中的异常被捕获记录
try:
process()
finally:
try:
cleanup()
except Exception:
logger.warning("Cleanup failed", exc_info=True)
以下类型的方法不应抛出异常:
__eq__)__hash__)__str__、__repr__)__del__)__exit__(应返回 False 而非抛异常)__bool__)在异步方法中,参数验证应在进入异步部分之前完成,确保调用者能立即感知错误。
# ✅ 参数验证在异步操作之前,异常立即抛出
async def fetch_data(url: str) -> str:
if not url:
raise ValueError("URL must not be empty.") # 立即抛出
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
return await response.text()
# ❌ 验证逻辑混在异步操作中,调用者难以立即感知
async def fetch_data(url: str) -> str:
async with aiohttp.ClientSession() as session:
if not url:
raise ValueError("URL must not be empty.") # 太晚了
async with session.get(url) as response:
return await response.text()
Python 中自定义异常类名应以 Error 结尾,并继承自合适的内置异常类:
# ✅ 命名正确,继承合理
class OrderProcessingError(Exception): ...
class InsufficientFundsError(ValueError): ...
class DatabaseConnectionError(ConnectionError): ...
# ❌ 命名不规范
class OrderProblem(Exception): ...
class BadFunds(Exception): ...
自定义异常应支持常见的构造模式:
class OrderProcessingError(Exception):
"""订单处理过程中发生的异常"""
def __init__(
self,
message: str = "Order processing failed.",
order_id: str | None = None,
cause: Exception | None = None,
):
super().__init__(message)
self.order_id = order_id
if cause is not None:
self.__cause__ = cause
仅当额外信息在编程场景中有用时才添加自定义属性:
# ✅ 提供有助于程序化处理的额外属性
class TransferError(Exception):
def __init__(self, message, *, from_account=None, to_account=None, amount=None):
super().__init__(message)
self.from_account = from_account
self.to_account = to_account
self.amount = amount
# 调用者可以根据属性做出决策
try:
transfer_funds(acct_a, acct_b, 1000)
except TransferError as e:
logger.error(f"Transfer of {e.amount} from {e.from_account} failed: {e}")
if e.amount > THRESHOLD:
alert_admin(e)
设计或审查异常处理策略时逐项确认:
处理层面:
except 块中异常从最具体到最通用排序with 语句自动管理资源with 管理的资源在 finally 块中清理dict.get()、getattr())asyncio.CancelledError 并重新传播重抛层面:
raise 而非 raise e(保留原始堆栈)raise ... from e(保留因果链)抛出层面:
finally 块中不抛出异常__eq__、__hash__、__str__、__del__ 等方法中不抛出异常自定义异常层面:
Error 结尾testing
智能体 UAT 验收测试技能。用于验证智能体在真实场景下的表现是否满足预期。支持任意智能体框架(langchain、langgraph、deepagents、crewai 等)。触发词:测试智能体、验收测试、agent test、UAT
tools
Use when you need to create a Gitea issue, update its spec/plan markers, read or merge an issue's state JSON, or post a PR review comment in a repo that uses the spx CLI (superpowers-vscode workflow).
development
Use when implementing, modifying, refactoring, or reviewing code and the agent must follow explicit coding standards for simplicity, readability, maintainability, testability, project conventions, and minimal safe changes.
development
Use when integrating the deepagents SDK into a Python project — creating agents, configuring backends, adding subagents, middleware, memory, or skills. Also use when debugging deepagents agents or choosing between StateBackend, FilesystemBackend, and LocalShellBackend.