跳转到内容

异常处理规范

异常分级、捕获、传播、全局异常处理


[Standard] 所有自定义异常必须继承自项目统一基类,并按业务域划分异常层级。

Exception(Python 内置)
└── AppError(项目基类,所有自定义异常的父类)
├── BusinessError(业务异常,可预期,需返回用户)
│ ├── ValidationError(参数校验失败)
│ ├── NotFoundError(资源不存在)
│ └── PermissionDeniedError(权限不足)
└── SystemError(系统异常,非预期,需告警)
├── DatabaseError
├── RemoteCallError
└── TimeoutError
级别说明处理方式HTTP 状态码
业务异常用户操作不当导致,可恢复提示用户,不告警4xx
系统异常依赖服务故障等,可重试重试 / 降级503
未知异常代码 bug,不可预期记录堆栈,告警500

[Requirement] 自定义异常类必须以 Error 结尾,且包含错误码(code)和用户可读消息(message)两个字段。

示例:

class AppError(Exception):
    code: str = "INTERNAL_ERROR"
    message: str = "系统内部错误"
 
    def __init__(self, message: str | None = None):
        if message:
            self.message = message
        super().__init__(self.message)
 
class BusinessError(AppError):
    code = "BUSINESS_ERROR"
 
class NotFoundError(BusinessError):
    code = "NOT_FOUND"
    message = "资源不存在"

[Requirement] 禁止裸 except:(捕获所有异常包括 KeyboardInterrupt),必须指定具体异常类型。

[Caution] 禁止捕获 Exception 后直接吞掉(pass),至少要记录日志。

[Standard] 只捕获你能处理的异常,不能处理的让它向上传播。

示例:

# 正确:只捕获能处理的异常
def load_config(path: str) -> dict:
    try:
        with open(path) as f:
            return yaml.safe_load(f)
    except FileNotFoundError:
        logger.warning("配置文件不存在,使用默认配置: %s", path)
        return {}
    except yaml.YAMLError as e:
        logger.error("配置文件解析失败: %s", path, exc_info=True)
        raise AppError("配置文件格式错误") from e

[Requirement] 捕获异常后必须记录日志,日志中包含异常堆栈(exc_info=True 或 logger.exception)。

[Standard] 重新抛出异常时使用 raise ... from e 保留原始异常链。

反模式:

# 错误:裸捕获并吞掉
try:
    risky_call()
except:
    pass
 
# 错误:捕获 Exception 后继续
try:
    risky_call()
except Exception:
    print("出错了")

[Standard] 底层异常不应直接暴露给上层调用方,应在边界层(如 Service 层)转换为业务异常。

[Requirement] 跨进程/跨服务调用时,必须将底层异常(如 HTTP 错误、数据库错误)转换为领域异常,避免泄漏实现细节。

示例:

# 正确:边界层转换异常
class UserService:
    def get_user(self, user_id: int) -> User:
        try:
            return self.repo.get(user_id)
        except sqlalchemy.exc.NoResultFound:
            raise NotFoundError(f"用户 {user_id} 不存在")
        except sqlalchemy.exc.DBAPIError as e:
            logger.error("数据库查询失败", exc_info=True)
            raise SystemError("数据库异常") from e

[Default] 对网络调用、消息消费等可重试场景,使用指数退避策略,最多重试 3 次。

[Caution] 只有幂等操作才能重试;非幂等操作(如扣款、下单)禁止自动重试。

import backoff
 
@backoff.on_exception(backoff.expo, requests.RequestException, max_tries=3)
def call_remote_api(url: str) -> dict:
    resp = requests.get(url, timeout=5)
    resp.raise_for_status()
    return resp.json()

[Requirement] Web 应用必须配置全局异常处理器,统一捕获未处理的异常,返回标准错误响应。

[Standard] 全局异常处理器不应吞掉未知异常时,必须记录完整堆栈并触发告警。

{
  "code": "USER_NOT_FOUND",
  "message": "用户不存在",
  "request_id": "req-abc123",
  "timestamp": "2024-01-01T00:00:00Z"
}

示例(FastAPI):

@app.exception_handler(AppError)
async def app_error_handler(request: AppError):
    logger.exception("业务异常: code=%s", exc.code)
    return JSONResponse(
        status_code=exc.http_status,
        content={"code": exc.code, "message": exc.message,
                 "request_id": request.state.request_id},
    )
 
@app.exception_handler(Exception)
async def unhandled_error_handler(request, exc):
    logger.exception("未处理异常", exc_info=True)
    alert.send(exc)  # 触发告警
    return JSONResponse(
        status_code=500,
        content={"code": "INTERNAL_ERROR", "message": "系统内部错误",
                 "request_id": request.state.request_id},
    )

反模式示例问题
裸 exceptexcept: pass吞掉所有异常,包括 KeyboardInterrupt,难以调试
捕获 Exception 后吞掉except Exception: pass隐藏错误,问题无法发现
仅打印不抛出except Exception as e: print(e)没有堆栈,丢失上下文
异常当控制流try: ... except ValueError: ... 用于正常分支判断异常机制效率低且语义不清,应用 if 判断
过度宽泛的捕获except Exception 然后什么都做应该只捕获能处理的类型
丢失原始异常raise NewError("msg") 不加 from异常链断裂,根因丢失
返回错误码代替异常return None 表示失败调用方容易忘记检查,应抛异常
在循环里捕获 Exceptionfor x in ...: try: ... except: pass静默失败,数据不一致