FastAPI 全局异常处理最佳实践:自定义异常、统一响应、兜底处理

在 FastAPI 项目里,如果每个接口都写一堆 try...except,项目刚开始可能还能接受,接口一多就会变得很乱:

  • 每个接口返回的错误格式不一致。
  • controller、service、crud 的职责边界变模糊。
  • 前端不好统一处理错误提示。
  • 日志记录分散,线上问题不好排查。
  • 系统异常可能直接暴露给前端,存在安全风险。

更推荐的方式是:controller / service / crud 只写正常业务流程,业务异常统一 raise,最后由 FastAPI 的全局异常处理器统一转换成响应

本文会用一套比较贴近实际项目的写法,整理 FastAPI 全局异常处理的设计思路和代码模板。

一、本文适合什么场景

本文适合下面这些场景:

  • FastAPI 项目想统一接口返回格式。
  • 不想在每个接口里重复写 try...except
  • 希望业务异常、参数异常、系统异常分开处理。
  • 希望 controller、service、crud 分层更清楚。
  • 希望项目里有一套可以复用的异常处理模板。

示例不绑定具体 ORM。你用 SQLAlchemy、Tortoise ORM、SQLModel 或其他数据库工具,都可以按这个思路改造。

二、为什么需要全局异常处理

先看一种不太推荐的写法:

python 复制代码
@router.post("/users")
async def create_user_api(form: UserCreate):
    try:
        user = await create_user(form)
        return {
            "success": True,
            "code": 0,
            "message": "创建成功",
            "data": user,
        }
    except Exception as exc:
        return JSONResponse(
            status_code=500,
            content={
                "success": False,
                "code": 50000,
                "message": str(exc),
            },
        )

这段代码的问题是:

  • controller 既处理请求,又处理异常响应,职责太重。
  • except Exception 太宽泛,容易把业务异常和系统异常混在一起。
  • str(exc) 可能把数据库错误、文件路径、内部实现细节暴露给前端。
  • 每个接口都这样写,会产生大量重复代码。

更好的做法是:

python 复制代码
@router.post("/users")
async def create_user_api(form: UserCreate):
    user = await create_user(form)
    return success_response(data=user, message="创建用户成功")

controller 只关心正常流程,异常交给全局异常处理器。

一句话总结:

正常流程写在业务代码里,异常响应统一交给 exception handler。

三、FastAPI 官方异常机制

FastAPI 本身已经提供了异常处理机制,主要有几个常用点:

机制 作用
HTTPException FastAPI 内置异常,适合返回 HTTP 错误
RequestValidationError 请求参数校验失败时触发
@app.exception_handler(...) 注册全局异常处理器
app.add_exception_handler(...) 另一种集中注册异常处理器的方式

FastAPI 官方文档里也强调:HTTPException 是一个普通的 Python 异常,只是带了 HTTP 响应相关信息。你可以在接口函数、service、依赖函数、工具函数里 raise 它,请求会被中断,然后由异常处理器生成响应。

项目里如果需要统一响应格式,就可以通过 @app.exception_handler(...)app.add_exception_handler(...) 覆盖默认异常响应。

四、异常分类

项目中不要把所有错误都当成一种异常。一般可以分成四类。

1. 业务异常

业务异常指的是:代码运行没有问题,但是业务规则不允许继续执行

比如:

  • 用户不存在
  • 用户名已存在
  • 密码错误
  • 余额不足
  • 库存不足
  • 订单状态不允许取消

这种异常适合定义成自己的业务异常类,例如 BusinessException

2. 参数异常

参数异常指的是请求参数不符合接口要求。

比如:

  • 必填字段没传
  • 字段类型错误
  • 字符串长度不符合要求
  • 枚举值不合法
  • 路径参数类型错误

FastAPI 结合 Pydantic 做参数校验时,如果请求参数不合法,会触发 RequestValidationError

3. 认证和权限异常

这类异常一般和登录、token、角色权限有关。

比如:

  • 未登录
  • token 过期
  • token 无效
  • 没有接口访问权限
  • 没有资源操作权限

可以直接使用 HTTPException(status_code=401)HTTPException(status_code=403),也可以根据项目习惯封装成业务异常。

4. 系统异常

系统异常是代码运行过程中出现的非预期错误。

比如:

  • 数据库连接失败
  • Redis 连接失败
  • 第三方接口超时
  • 文件读写失败
  • 未捕获的 Python 异常

系统异常不应该把原始异常信息直接返回给前端。更合理的方式是:日志里记录真实异常,前端只收到统一的错误提示

五、推荐项目结构

可以按下面这种结构组织代码:

python 复制代码
app/
├── main.py
├── core/
│   ├── response.py
│   ├── exceptions.py
│   └── exception_handlers.py
├── api/
│   └── user.py
├── services/
│   └── user_service.py
├── crud/
│   └── user_crud.py
└── schemas/
    └── user.py

核心文件说明:

文件 作用
response.py 统一成功和失败响应格式
exceptions.py 定义自定义业务异常
exception_handlers.py 定义全局异常处理器
main.py 注册异常处理器
services/ 写业务流程,业务不成立时 raise
crud/ 写数据库操作,尽量保持简单

六、统一响应格式

为了让前端更好处理,建议接口统一返回类似结构:

python 复制代码
{
  "success": false,
  "code": 40001,
  "message": "用户名已存在",
  "data": null
}

字段说明:

字段 含义
success 业务是否成功
code 业务状态码
message 错误提示或成功提示
data 业务数据

这里要注意区分两个概念:

  • status_code:HTTP 状态码,比如 200、400、401、403、404、422、500。
  • code:业务状态码,比如 40001 表示用户名已存在。

不要把 HTTP 状态码和业务状态码混在一起。HTTP 状态码表示请求层面的结果,业务 code 表示项目内部约定的业务结果。

七、代码模板

下面给出一套可以直接放进项目里的基础模板。

1. 统一响应工具

python 复制代码
# app/core/response.py

from typing import Any


def success_response(
    data: Any = None,
    message: str = "success",
    code: int = 0,
) -> dict:
    return {
        "success": True,
        "code": code,
        "message": message,
        "data": data,
    }


def error_response(
    message: str,
    code: int,
    data: Any = None,
) -> dict:
    return {
        "success": False,
        "code": code,
        "message": message,
        "data": data,
    }

2. 自定义业务异常

python 复制代码
# app/core/exceptions.py

from http import HTTPStatus


class BusinessException(Exception):
    def __init__(
        self,
        message: str,
        code: int = 40000,
        status_code: int = HTTPStatus.BAD_REQUEST,
    ):
        self.message = message
        self.code = code
        self.status_code = status_code

这个异常类只保存异常信息,不负责返回 JSONResponse

字段说明:

字段 作用
message 返回给前端的错误提示
code 业务错误码
status_code HTTP 状态码

3. 全局异常处理器

python 复制代码
# app/core/exception_handlers.py

import logging

from fastapi import FastAPI, Request
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from starlette.exceptions import HTTPException as StarletteHTTPException

from app.core.exceptions import BusinessException
from app.core.response import error_response


logger = logging.getLogger(__name__)


async def business_exception_handler(
    request: Request,
    exc: BusinessException,
) -> JSONResponse:
    return JSONResponse(
        status_code=exc.status_code,
        content=error_response(
            code=exc.code,
            message=exc.message,
        ),
    )


async def validation_exception_handler(
    request: Request,
    exc: RequestValidationError,
) -> JSONResponse:
    return JSONResponse(
        status_code=422,
        content=error_response(
            code=42200,
            message="请求参数校验失败",
            data=exc.errors(),
        ),
    )


async def http_exception_handler(
    request: Request,
    exc: StarletteHTTPException,
) -> JSONResponse:
    return JSONResponse(
        status_code=exc.status_code,
        content=error_response(
            code=exc.status_code,
            message=str(exc.detail),
        ),
    )


async def unknown_exception_handler(
    request: Request,
    exc: Exception,
) -> JSONResponse:
    logger.exception("未处理的系统异常: %s %s", request.method, request.url)

    return JSONResponse(
        status_code=500,
        content=error_response(
            code=50000,
            message="服务器内部错误",
        ),
    )


def register_exception_handlers(app: FastAPI) -> None:
    app.add_exception_handler(BusinessException, business_exception_handler)
    app.add_exception_handler(RequestValidationError, validation_exception_handler)
    app.add_exception_handler(StarletteHTTPException, http_exception_handler)
    app.add_exception_handler(Exception, unknown_exception_handler)

这里用了 StarletteHTTPException,是因为 FastAPI 底层基于 Starlette。这样不仅能处理 FastAPI 主动抛出的 HTTPException,也能处理 Starlette 内部或相关扩展抛出的 HTTP 异常。

4. 在 main.py 中注册

python 复制代码
# app/main.py

from fastapi import FastAPI

from app.core.exception_handlers import register_exception_handlers


app = FastAPI()

register_exception_handlers(app)

这样,全局异常处理器就注册好了。

八、分层怎么写

全局异常处理不是单独存在的,它要配合项目分层一起使用。

1. controller 层

controller 负责接收请求、调用 service、返回结果。

controller 不要写大量业务判断,也不要到处写 try...except

python 复制代码
# app/api/user.py

from fastapi import APIRouter

from app.core.response import success_response
from app.schemas.user import UserCreate
from app.services.user_service import create_user


router = APIRouter(prefix="/users", tags=["用户"])


@router.post("")
async def create_user_api(form: UserCreate):
    user = await create_user(form)
    return success_response(data=user, message="创建用户成功")

controller 这层保持简单,异常不在这里展开处理。

2. service 层

service 负责业务规则判断。业务不成立时,直接抛出业务异常。

python 复制代码
# app/services/user_service.py

from app.core.exceptions import BusinessException
from app.crud.user_crud import get_user_by_username, save_user
from app.schemas.user import UserCreate


async def create_user(form: UserCreate):
    exists_user = await get_user_by_username(form.username)

    if exists_user:
        raise BusinessException(
            message="用户名已存在",
            code=40001,
            status_code=400,
        )

    return await save_user(form)

这段代码的业务流程很清楚:

复制代码
查询用户名是否存在 -> 已存在就抛业务异常 -> 不存在就创建用户

不需要在 service 里直接返回 JSONResponse

3. crud 层

crud 负责数据库操作,尽量保持简单。

python 复制代码
# app/crud/user_crud.py

async def get_user_by_username(username: str):
    return await User.filter(username=username).first()


async def save_user(form):
    return await User.create(**form.model_dump())

如果你使用的是 Pydantic v1,可以把 model_dump() 换成 dict()

python 复制代码
return await User.create(**form.dict())

crud 层一般不写复杂业务判断。除非是数据库唯一约束、事务回滚、连接异常这类和数据访问强相关的逻辑,否则业务规则建议放在 service 层。

九、兜底异常处理为什么重要

很多项目一开始只处理业务异常,但没有处理未知异常。这样线上一旦出现未预期错误,可能会返回很不稳定的响应,甚至暴露内部信息。

兜底异常处理器的作用是:

  • 后端日志记录真实异常。
  • 前端收到统一的错误格式。
  • 避免把数据库错误、文件路径、堆栈信息暴露给用户。

推荐写法:

python 复制代码
async def unknown_exception_handler(
    request: Request,
    exc: Exception,
) -> JSONResponse:
    logger.exception("未处理的系统异常: %s %s", request.method, request.url)

    return JSONResponse(
        status_code=500,
        content=error_response(
            code=50000,
            message="服务器内部错误",
        ),
    )

不推荐这样写:

python 复制代码
return JSONResponse(
    status_code=500,
    content={"message": str(exc)},
)

原因很简单:str(exc) 不是给前端看的,它可能包含数据库表名、SQL、文件路径、第三方服务返回的敏感信息。

十、什么时候才需要 try...except

全局异常处理不代表项目里完全不能写 try...except

适合写 try...except 的场景包括:

  • 调用第三方接口,需要把超时转换成业务异常。
  • 操作文件、消息队列、Redis 等外部资源。
  • 需要做失败补偿,比如回滚、释放资源。
  • 需要记录更详细的上下文日志。

例如调用支付服务:

python 复制代码
async def call_payment_api(order_id: int):
    try:
        return await payment_client.pay(order_id)
    except TimeoutError as exc:
        raise BusinessException(
            message="支付服务超时,请稍后重试",
            code=50010,
            status_code=503,
        ) from exc

这里有两个关键点:

  • 捕获异常后不要直接吞掉。
  • 使用 raise ... from exc 保留异常链,方便排查原始错误。

十一、不同异常应该返回什么状态码

可以参考下面这个表:

场景 HTTP 状态码 说明
参数校验失败 422 或 400 FastAPI 默认常用 422
未登录 401 没有有效身份认证
无权限 403 已登录但无权限
资源不存在 404 查询对象不存在
业务规则不通过 400 比如用户名已存在、余额不足
第三方服务不可用 503 比如支付服务超时
未知系统异常 500 服务器内部错误

业务状态码可以自己约定,比如:

业务 code 含义
0 成功
40000 通用业务错误
40001 用户名已存在
40100 未登录
40300 无权限
42200 参数校验失败
50000 服务器内部错误

具体编号不重要,重要的是团队内部保持一致。

十二、常见错误

1. 每个接口都写 try...except

这样会导致重复代码很多,而且每个接口的错误格式容易不一致。

更好的方式是:业务错误抛 BusinessException,统一异常处理器负责返回响应。

2. service 里直接返回 JSONResponse

service 层不应该关心 HTTP 响应。它应该只关心业务流程。

不推荐:

python 复制代码
async def create_user(form):
    if exists_user:
        return JSONResponse(...)

推荐:

python 复制代码
async def create_user(form):
    if exists_user:
        raise BusinessException(message="用户名已存在", code=40001)

3. 把所有异常都返回 500

用户名已存在、参数错误、权限不足都不是 500。500 表示服务器内部错误,不适合表示普通业务失败。

4. 把原始异常直接返回给前端

错误信息应该分两层:

  • 日志里记录真实异常。
  • 前端只返回安全、稳定、可理解的提示。

5. crud 层写大量业务判断

crud 层负责数据访问,service 层负责业务规则。把大量业务判断放在 crud 层,会让代码后期很难复用。

十三、完整流程回顾

整个请求流程可以理解成:

python 复制代码
前端请求
  -> controller 接收参数
  -> service 执行业务流程
  -> crud 查询或写入数据库
  -> 如果业务不成立,service raise BusinessException
  -> exception handler 捕获异常
  -> 返回统一 JSON 响应

职责可以总结成下面这张表:

层级 职责 是否建议处理异常
controller 接收请求、调用 service 不建议大量 try...except
service 写业务流程和业务判断 可以 raise BusinessException
crud 数据库操作 尽量保持简单
exception handler 统一转换异常响应 专门处理异常
logger 记录真实错误 记录系统异常细节

十四、总结

FastAPI 全局异常处理的重点不是"把所有错误都捕获掉",而是让项目的错误处理有清晰边界。

推荐原则:

  • controller 只写接口入口。
  • service 写业务流程,业务不成立就 raise
  • crud 只写数据访问,不混入 HTTP 响应。
  • 自定义业务异常只保存错误信息。
  • exception handler 统一把异常转换成响应。
  • 未知异常要兜底,并记录日志。

最终目标是:

业务代码保持 basic flow,异常逻辑集中处理,前端响应格式稳定,线上问题可以通过日志排查。

参考资料

相关推荐
PAK向日葵2 小时前
我用 C++ 写了一个轻量级 Python 虚拟机,刚刚开源
c++·python·开源
财经资讯数据_灵砚智能4 小时前
基于全球经济类多源新闻的NLP情感分析与数据可视化(日间)2026年5月26日
大数据·人工智能·python·信息可视化·自然语言处理·ai编程·灵砚智能
我材不敲代码4 小时前
Python基础:列表详解、增删改查及常用高阶操作
开发语言·windows·python
li星野4 小时前
FastAPI 入门:异步与同步端点的性能差异与并发测试解析
fastapi
AI玫瑰助手4 小时前
Python运算符:成员运算符(in/not in)的使用场景
开发语言·python·信息可视化
Warson_L5 小时前
python - class 入门
python
水木流年追梦5 小时前
大模型入门-大模型分布式训练2
开发语言·分布式·python·算法·正则表达式·prompt
ZHANG8023ZHEN5 小时前
Diffusion 数学推理
人工智能·python·机器学习
海天一色y5 小时前
SGLang 本地部署 Qwen3-8B 大模型实战指南
python·sglang
代码帮6 小时前
面试题 - GIL全局解释器锁 :为什么Python多线程不能利用多核?GIL对I/O密集和CPU密集任务的影响?如何绕过GIL(多进程、C扩展)
python·面试