tRPC 错误处理 #
错误处理概览 #
tRPC 提供了完整的错误处理机制,包括服务端错误抛出、错误类型定义、客户端错误捕获等。
text
┌─────────────────────────────────────────────────────────────┐
│ tRPC 错误流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 服务端 │
│ ┌─────────────────────────────────────────────┐ │
│ │ Procedure / Middleware │ │
│ │ throw new TRPCError({ ... }) │ │
│ └─────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────┐ │
│ │ tRPC 格式化错误 │ │
│ │ { │ │
│ │ error: { │ │
│ │ message: string, │ │
│ │ code: TRPC_ERROR_CODE, │ │
│ │ data: { ... } │ │
│ │ } │ │
│ │ } │ │
│ └─────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 客户端 │
│ ┌─────────────────────────────────────────────┐ │
│ │ 捕获错误 │ │
│ │ try/catch 或 onError 回调 │ │
│ └─────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
TRPCError #
错误类型 #
tRPC 定义了以下错误代码:
typescript
type TRPCErrorCodes =
| 'PARSE_ERROR'
| 'BAD_REQUEST'
| 'UNAUTHORIZED'
| 'FORBIDDEN'
| 'NOT_FOUND'
| 'METHOD_NOT_SUPPORTED'
| 'TIMEOUT'
| 'CONFLICT'
| 'PRECONDITION_FAILED'
| 'PAYLOAD_TOO_LARGE'
| 'UNSUPPORTED_MEDIA_TYPE'
| 'UNPROCESSABLE_CONTENT'
| 'TOO_MANY_REQUESTS'
| 'CLIENT_CLOSED_REQUEST'
| 'INTERNAL_SERVER_ERROR';
text
┌─────────────────────────────────────────────────────────────┐
│ 错误代码对照表 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 错误代码 HTTP 状态码 说明 │
│ ───────────────────────────────────────────────────────── │
│ PARSE_ERROR 400 解析错误 │
│ BAD_REQUEST 400 请求错误 │
│ UNAUTHORIZED 401 未授权 │
│ FORBIDDEN 403 禁止访问 │
│ NOT_FOUND 404 未找到 │
│ METHOD_NOT_SUPPORTED 405 方法不支持 │
│ TIMEOUT 408 请求超时 │
│ CONFLICT 409 冲突 │
│ PRECONDITION_FAILED 412 前置条件失败 │
│ PAYLOAD_TOO_LARGE 413 负载过大 │
│ TOO_MANY_REQUESTS 429 请求过多 │
│ CLIENT_CLOSED_REQUEST 499 客户端关闭请求 │
│ INTERNAL_SERVER_ERROR 500 服务器内部错误 │
│ │
└─────────────────────────────────────────────────────────────┘
创建错误 #
typescript
import { TRPCError } from '@trpc/server';
const router = t.router({
user: t.router({
getById: t.procedure
.input(z.object({ id: z.string() }))
.query(async ({ input }) => {
const user = await db.user.findUnique({
where: { id: input.id },
});
if (!user) {
throw new TRPCError({
code: 'NOT_FOUND',
message: `User with id ${input.id} not found`,
});
}
return user;
}),
create: t.procedure
.input(createUserSchema)
.mutation(async ({ input }) => {
const existing = await db.user.findUnique({
where: { email: input.email },
});
if (existing) {
throw new TRPCError({
code: 'CONFLICT',
message: 'Email already exists',
});
}
return db.user.create({ data: input });
}),
}),
});
错误属性 #
typescript
interface TRPCErrorOptions {
code: TRPCErrorCodes;
message?: string;
cause?: Error;
}
class TRPCError extends Error {
public readonly cause?: Error;
public readonly code: TRPCErrorCodes;
constructor(options: TRPCErrorOptions) {
super(options.message ?? options.code);
this.cause = options.cause;
this.code = options.code;
}
}
服务端错误处理 #
基本错误抛出 #
typescript
const router = t.router({
user: t.router({
delete: t.procedure
.input(z.object({ id: z.string() }))
.mutation(async ({ input, ctx }) => {
if (!ctx.user) {
throw new TRPCError({
code: 'UNAUTHORIZED',
message: 'You must be logged in to delete a user',
});
}
const user = await db.user.findUnique({
where: { id: input.id },
});
if (!user) {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'User not found',
});
}
if (ctx.user.id !== input.id && ctx.user.role !== 'admin') {
throw new TRPCError({
code: 'FORBIDDEN',
message: 'You can only delete your own account',
});
}
await db.user.delete({ where: { id: input.id } });
return { success: true };
}),
}),
});
错误与原始异常 #
typescript
const router = t.router({
user: t.router({
create: t.procedure
.input(createUserSchema)
.mutation(async ({ input }) => {
try {
return await db.user.create({ data: input });
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
if (error.code === 'P2002') {
throw new TRPCError({
code: 'CONFLICT',
message: 'Email already exists',
cause: error,
});
}
}
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: 'Failed to create user',
cause: error,
});
}
}),
}),
});
验证错误 #
typescript
import { z } from 'zod';
const router = t.router({
user: t.router({
update: t.procedure
.input(z.object({
id: z.string(),
email: z.string().email(),
}))
.mutation(async ({ input }) => {
const result = await updateUser(input);
if (!result.success) {
throw new TRPCError({
code: 'BAD_REQUEST',
message: result.error,
});
}
return result.data;
}),
}),
});
自定义错误数据 #
typescript
interface CustomErrorData {
code: string;
details: Record<string, any>;
stack?: string;
}
const router = t.router({
user: t.router({
create: t.procedure
.input(createUserSchema)
.mutation(async ({ input }) => {
const validationErrors = validateUser(input);
if (validationErrors.length > 0) {
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'Validation failed',
cause: new Error(JSON.stringify({
code: 'VALIDATION_ERROR',
details: validationErrors,
})),
});
}
return db.user.create({ data: input });
}),
}),
});
错误中间件 #
typescript
const errorMiddleware = t.middleware(async ({ next }) => {
try {
return await next();
} catch (error) {
if (error instanceof TRPCError) {
throw error;
}
if (error instanceof z.ZodError) {
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'Validation failed',
cause: error,
});
}
if (error instanceof Prisma.PrismaClientKnownRequestError) {
switch (error.code) {
case 'P2002':
throw new TRPCError({
code: 'CONFLICT',
message: 'Record already exists',
cause: error,
});
case 'P2025':
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Record not found',
cause: error,
});
}
}
console.error('Unexpected error:', error);
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: 'An unexpected error occurred',
cause: error,
});
}
});
export const publicProcedure = t.procedure.use(errorMiddleware);
客户端错误处理 #
try/catch #
typescript
import { TRPCClientError } from '@trpc/client';
import type { AppRouter } from '../server/routers';
async function createUser(data: CreateUserInput) {
try {
const user = await client.user.create.mutate(data);
return { success: true, user };
} catch (error) {
if (error instanceof TRPCClientError) {
console.error('tRPC Error:', error.message);
console.error('Error code:', error.data?.code);
return {
success: false,
error: error.message,
code: error.data?.code,
};
}
throw error;
}
}
React 错误处理 #
tsx
import { trpc } from './trpc';
function UserList() {
const { data, error, isError, isLoading } = trpc.user.list.useQuery();
if (isLoading) {
return <div>Loading...</div>;
}
if (isError) {
return (
<div className="error">
<h2>Error</h2>
<p>{error.message}</p>
<p>Code: {error.data?.code}</p>
</div>
);
}
return (
<ul>
{data?.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
Mutation 错误处理 #
tsx
function CreateUser() {
const utils = trpc.useUtils();
const createUser = trpc.user.create.useMutation({
onSuccess: (data) => {
utils.user.list.invalidate();
toast.success(`User ${data.name} created!`);
},
onError: (error) => {
switch (error.data?.code) {
case 'CONFLICT':
toast.error('Email already exists');
break;
case 'BAD_REQUEST':
toast.error('Invalid input data');
break;
case 'UNAUTHORIZED':
toast.error('Please log in first');
break;
default:
toast.error(`Error: ${error.message}`);
}
},
});
return (
<form onSubmit={(e) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
createUser.mutate({
name: formData.get('name') as string,
email: formData.get('email') as string,
});
}}>
<input name="name" placeholder="Name" />
<input name="email" placeholder="Email" />
<button type="submit" disabled={createUser.isPending}>
Create
</button>
{createUser.isError && (
<p className="error">{createUser.error.message}</p>
)}
</form>
);
}
全局错误处理 #
tsx
import { QueryCache, QueryClient } from '@tanstack/react-query';
import { TRPCClientError } from '@trpc/client';
const queryClient = new QueryClient({
queryCache: new QueryCache({
onError: (error) => {
if (error instanceof TRPCClientError) {
if (error.data?.code === 'UNAUTHORIZED') {
window.location.href = '/login';
}
}
},
}),
mutationCache: new MutationCache({
onError: (error) => {
if (error instanceof TRPCClientError) {
toast.error(error.message);
}
},
}),
});
错误边界 #
tsx
import { ErrorBoundary } from 'react-error-boundary';
function ErrorFallback({ error, resetErrorBoundary }: {
error: Error;
resetErrorBoundary: () => void;
}) {
return (
<div className="error-boundary">
<h2>Something went wrong</h2>
<p>{error.message}</p>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
);
}
function App() {
return (
<ErrorBoundary FallbackComponent={ErrorFallback}>
<UserList />
</ErrorBoundary>
);
}
错误格式化 #
自定义错误格式 #
typescript
import { initTRPC, TRPCError } from '@trpc/server';
const t = initTRPC.create({
errorFormatter({ shape, error }) {
return {
...shape,
data: {
...shape.data,
customError: {
code: error.code,
message: error.message,
timestamp: new Date().toISOString(),
path: shape.path,
},
},
};
},
});
国际化错误消息 #
typescript
const errorMessages: Record<string, Record<string, string>> = {
en: {
UNAUTHORIZED: 'Please log in to continue',
FORBIDDEN: 'You do not have permission',
NOT_FOUND: 'The requested resource was not found',
CONFLICT: 'This resource already exists',
},
zh: {
UNAUTHORIZED: '请先登录',
FORBIDDEN: '没有权限',
NOT_FOUND: '请求的资源不存在',
CONFLICT: '资源已存在',
},
};
const t = initTRPC.create({
errorFormatter({ shape, error, ctx }) {
const locale = ctx?.locale ?? 'en';
return {
...shape,
message: errorMessages[locale]?.[error.code] ?? error.message,
};
},
});
结构化错误 #
typescript
interface StructuredError {
code: string;
message: string;
details?: Array<{
field: string;
message: string;
}>;
}
const router = t.router({
user: t.router({
create: t.procedure
.input(createUserSchema)
.mutation(async ({ input }) => {
const errors: Array<{ field: string; message: string }> = [];
if (await isEmailTaken(input.email)) {
errors.push({
field: 'email',
message: 'Email is already taken',
});
}
if (input.name.length < 2) {
errors.push({
field: 'name',
message: 'Name must be at least 2 characters',
});
}
if (errors.length > 0) {
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'Validation failed',
cause: new Error(JSON.stringify({
code: 'VALIDATION_ERROR',
details: errors,
})),
});
}
return db.user.create({ data: input });
}),
}),
});
错误恢复 #
重试机制 #
tsx
function UserList() {
const { data, error, isError, refetch } = trpc.user.list.useQuery(
undefined,
{
retry: 3,
retryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 30000),
}
);
if (isError) {
return (
<div>
<p>Error: {error.message}</p>
<button onClick={() => refetch()}>Retry</button>
</div>
);
}
return <ul>{/* ... */}</ul>;
}
自动重连 #
typescript
import { retryLink } from '@trpc/client';
const client = createTRPCProxyClient<AppRouter>({
links: [
retryLink({
maxAttempts: 3,
retry: (opts) => {
if (opts.error.data?.code === 'UNAUTHORIZED') {
return false;
}
return opts.attempts < 3;
},
}),
httpBatchLink({
url: 'http://localhost:4000/trpc',
}),
],
});
优雅降级 #
tsx
function UserList() {
const { data, error, isError } = trpc.user.list.useQuery();
if (isError) {
return (
<div>
<p>Unable to load users. Showing cached data.</p>
<CachedUserList />
</div>
);
}
return <ul>{/* ... */}</ul>;
}
调试技巧 #
详细日志 #
typescript
const debugMiddleware = t.middleware(async ({ path, type, input, next }) => {
console.log('Request:', { path, type, input });
try {
const result = await next();
console.log('Response:', { path, result });
return result;
} catch (error) {
console.error('Error:', { path, error });
throw error;
}
});
错误追踪 #
typescript
import * as Sentry from '@sentry/node';
const sentryMiddleware = t.middleware(async ({ path, type, next }) => {
try {
return await next();
} catch (error) {
Sentry.captureException(error, {
tags: {
trpc_path: path,
trpc_type: type,
},
});
throw error;
}
});
开发环境错误 #
typescript
const t = initTRPC.create({
errorFormatter({ shape, error }) {
if (process.env.NODE_ENV === 'development') {
return {
...shape,
data: {
...shape.data,
stack: error.cause?.stack,
},
};
}
return shape;
},
});
最佳实践 #
1. 使用正确的错误代码 #
typescript
throw new TRPCError({
code: 'NOT_FOUND',
message: 'User not found',
});
throw new TRPCError({
code: 'UNAUTHORIZED',
message: 'Please log in',
});
throw new TRPCError({
code: 'FORBIDDEN',
message: 'Admin access required',
});
2. 提供有意义的错误消息 #
typescript
throw new TRPCError({
code: 'BAD_REQUEST',
message: `User with email ${input.email} already exists`,
});
3. 保留原始错误 #
typescript
try {
await someOperation();
} catch (error) {
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: 'Operation failed',
cause: error,
});
}
4. 统一错误处理 #
typescript
const errorHandlerMiddleware = t.middleware(async ({ next }) => {
try {
return await next();
} catch (error) {
if (error instanceof TRPCError) {
throw error;
}
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: 'An unexpected error occurred',
cause: error,
});
}
});
5. 客户端错误分类 #
typescript
function handleTRPCError(error: TRPCClientError<AppRouter>) {
switch (error.data?.code) {
case 'UNAUTHORIZED':
redirectToLogin();
break;
case 'FORBIDDEN':
showForbiddenMessage();
break;
case 'NOT_FOUND':
showNotFoundPage();
break;
default:
showGenericError(error.message);
}
}
下一步 #
现在你已经掌握了 tRPC 的错误处理机制,接下来学习 高级特性,了解 tRPC 的更多高级功能!
最后更新:2026-03-29