错误处理体系:错误码与重试策略
本教程共 54 篇 · 第 42 篇 · 更新于 2026-08-11 · 约 6 分钟阅读
本节目标:看懂 Prisma 错误分几类、错误码什么意思,把错误映射成 HTTP 语义并合理重试。
错误处理做得好不好,决定用户看到的是「邮箱已注册」还是「服务器开小差了」。
错误类层次
Prisma 的异常类型挂在 Prisma 命名空间下,用 instanceof 区分:
- PrismaClientKnownRequestError:数据库返回的已知错误,带 code 和 meta。最常见,比如唯一约束冲突 P2002。
- PrismaClientUnknownRequestError:没有对应错误码的请求错误。
- PrismaClientInitializationError:初始化失败,如连不上数据库、凭据错误。在
$connect()或首条查询时抛出,带 errorCode。 - PrismaClientValidationError:查询参数校验失败,比如缺必填字段、类型传错。
- PrismaClientRustPanicError:引擎崩溃。v7 移除了 Rust 引擎,这个错误基本见不到了。
import { PrismaPg } from "@prisma/adapter-pg";
import { PrismaClient, Prisma } from "../generated/prisma/client";
const prisma = new PrismaClient({
adapter: new PrismaPg({ connectionString: process.env.DATABASE_URL }),
});
try {
await prisma.user.create({ data: { email: "a@b.com" } });
} catch (e) {
if (e instanceof Prisma.PrismaClientKnownRequestError) {
if (e.code === "P2002") {
console.log("邮箱已存在:", e.meta?.target);
}
}
throw e;
}
TipPrisma 命名空间从生成的客户端导入:
import { PrismaClient, Prisma } from "../generated/prisma/client"。
错误码速查
P 开头的码按段分类:P1xxx 连接类,P2xxx 查询类,P3xxx 迁移类,P4xxx 内省类。日常最常遇到这几个:
| 错误码 | 含义 | 典型处理 |
|---|---|---|
| P1001 | 连不上数据库服务器 | 检查地址端口,可重试 |
| P1002 | 连上了但超时 | 重试 |
| P1008 | 操作超时 | 重试或调大超时 |
| P2000 | 值超出列类型长度 | 校验输入 |
| P2002 | 唯一约束冲突 | 转 409,提示已存在 |
| P2003 | 外键约束失败 | 转 400,提示关联不存在 |
| P2015 | 关联记录找不到 | 转 404 |
| P2024 | 连接池耗尽超时 | 降级或重试 |
| P2025 | 需要的记录不存在 | 转 404 |
| P2028 | 事务 API 错误 | 检查事务用法 |
| P2034 | 事务写冲突或死锁 | 整体重试事务 |
| P2037 | 数据库连接数过多 | 调池、加 PgBouncer |
错误对象上除了 code,还有 meta:唯一约束冲突时 meta.target 给出冲突的字段名,定位问题很省事。连接类错误还有 P1017(服务器关闭连接),常见于数据库重启或网络被切断。
P2025 值得单独说:findUnique 查不到返回 null 不报错;findUniqueOrThrow、update、delete 等操作找不到记录才抛 P2025。如果「查不到」是合法场景,改用 updateMany / deleteMany,它们匹配零条也不会报错,省一层 try/catch。
ValidationError 通常在编译期就被 TypeScript 拦住了。运行时还会遇到,多半是动态输入绕过了类型检查(比如从请求体直接构造 data)。这类错误是程序 bug,不该重试,记日志后转 500。
HTTP 语义映射
API 层最常见的映射:P2002 → 409 Conflict,P2025 → 404 Not Found,P2003 → 400 Bad Request,连接类错误 → 503 Service Unavailable。集中在一个错误处理中间件里做,别散落在每个路由:
function toHttpStatus(e: unknown): number {
if (e instanceof Prisma.PrismaClientKnownRequestError) {
switch (e.code) {
case "P2002":
return 409;
case "P2025":
case "P2015":
return 404;
case "P2003":
case "P2000":
return 400;
case "P2024":
case "P1001":
return 503;
}
}
return 500;
}
Note给用户的错误信息要克制,别把 meta、SQL 细节直接吐出去,防止泄露表结构。
自定义错误类解耦
业务层别到处依赖 Prisma 的错误码。定义自己的错误类,把 Prisma 错误翻译成业务语言,边界处再映射 HTTP:
export class EmailAlreadyExistsError extends Error {
constructor() {
super("Email already exists");
}
}
服务层捕获 P2002 抛 EmailAlreadyExistsError,路由层只认自己的错误类。以后换 ORM 或错误码变化,只有翻译层要改。
错误还要记日志。用 log 选项的 error 级别加事件订阅,把错误码、模型、操作一起记下来,配合 Sentry 之类的监控按 prisma.code 打标签,事后统计哪种错误最多一目了然:
prisma.$on("error", (e) => {
logger.error({ message: e.message, target: e.target });
});
瞬态错误重试
不是所有错误都该重试。重试只适用于瞬态错误:P1001(网络抖动)、P2024(池暂时耗尽)、P2034(写冲突或死锁,官方明确建议重试)。P2002、P2025 这类业务错误重试多少次都一样,直接抛出。
重试要带退避,别立刻连发:
async function retryOnConflict<T>(fn: () => Promise<T>, retries = 3): Promise<T> {
for (let attempt = 1; attempt <= retries; attempt++) {
try {
return await fn();
} catch (e) {
if ((e as { code?: string }).code !== "P2034" || attempt === retries) throw e;
await new Promise((r) => setTimeout(r, 100 * 2 ** attempt));
}
}
throw new Error("unreachable");
}
重试策略整体上分三类:瞬态错误重试;业务错误直接抛出并映射成 4xx;耗时操作(Saga、消息队列补偿)才考虑补偿动作。别把重试用在非幂等操作上,比如扣款,重复执行会出问题。交互式事务推荐包一层重试循环,专门对付 P2034。
errorFormat:控制错误长相
错误信息默认带颜色和排版建议,在 API 日志里很吵。三种格式:pretty(默认,彩色带修复建议)、colorless(无色)、minimal(纯文本)。构造函数里配:
const prisma = new PrismaClient({
adapter,
errorFormat: "minimal",
});
也可以靠环境变量:NODE_ENV=production 自动输出最小化错误,设置 NO_COLOR 去掉颜色。
最后把整套做法串成清单:用 instanceof 分层捕获 → 按错误码分流 → 业务错误映射 HTTP、瞬态错误重试 → 边界处翻译成自定义错误 → 统一记日志。按这个顺序写,错误处理就不会乱。
NoteMongoDB 相关错误(如 P2031 复制集要求)只出现在 v6,v7 不支持 MongoDB,留在 v6.19。
参考来源
- Prisma 官方文档:Error Reference / Handling exceptions and errors / Configuring error formatting
- Mapagam:Handling Prisma Errors
- DevSheets:Error Handling in Prisma
- Generalist Programmer:NestJS Prisma Tutorial Complete Guide