TypeScript自定义错误处理方案:是否具备可持续性?
Web应用错误处理方案的合理性与可持续性探讨
我想了解当前这套Web应用错误处理方式是否具备可持续性,存在哪些不当之处。以下是我的实现代码:
一、当前实现代码
1. 错误定义文件 src/errors/index.ts
interface BaseErrorProperties { name?: string; message: string; statusCode: number; action?: string; stack: string; key?: string; } class BaseError extends Error { public statusCode: number; public action?: string; public key?: string; constructor({ name, message, action, statusCode, stack, key }) { super(); this.message = message; this.name = name; this.action = action; this.statusCode = statusCode; this.stack = stack; this.key = key; } } class ServiceError extends BaseError { constructor({ message, action, statusCode, stack, key, }: BaseErrorProperties) { super({ name: "ServiceError", message: message || "A service error happened.", statusCode: statusCode || 503, action: action || "Please try again. If the error persists, contact the administrator.", stack: stack, key: null, }); } } class ValidationError extends BaseError { constructor({ message, action, statusCode, stack, key, }: BaseErrorProperties) { super({ name: "ValidationError", message: message || "A validation error happened", statusCode: statusCode || 400, action: action || "Please try again. If the error persists, contact the administrator.", stack: stack, key: key, }); } } export { ServiceError, ValidationError };
2. 邮件发送控制器代码
import { ValidationError } from "@/errors"; import email from "models/email"; import { z, ZodError } from "zod"; const emailRequestBodySchema = z.object({ from: z.string().email(), to: z.string().email(), subject: z.string(), text: z.string(), html: z.string(), }); export async function POST(request: Request) { try { const requestBody = await request.json(); // check for possible mistakes emailRequestBodySchema.parse(requestBody); email.sendMail(requestBody); return Response.json({ ok: true, }); } catch (error) { if (error instanceof ZodError) { const validationError = new ValidationError({ key: String(error.issues[0].path[0]), message: "Invalid data", stack: new Error().stack, statusCode: 400, action: "Check your data", }); return new Response( JSON.stringify({ error: validationError, }), { status: validationError.statusCode, }, ); } } }
补充说明:错误响应返回客户端后,我会渲染错误提示弹窗,而非仅在控制台打印错误信息。我知道还可以在控制器中增加更多错误捕获逻辑,但先聚焦现有方案的合理性。
二、方案分析与改进建议
1. 现有方案的可取之处
- 自定义错误类继承
Error,统一了错误结构(包含状态码、用户提示动作、错误关联字段等),完美适配前端弹窗展示的需求,整体逻辑清晰 - 区分
ServiceError和ValidationError两种核心错误类型,为后续不同场景的差异化处理预留了空间 - Zod校验失败时转换为自定义
ValidationError,保证了所有错误响应格式的一致性,前端无需适配多种错误结构
2. 潜在问题与可持续性隐患
- 错误栈丢失:构造
ValidationError时手动传入new Error().stack,会覆盖Zod错误的原始栈信息,后端排查问题时无法定位到校验失败的具体代码位置 - 错误覆盖不全:控制器仅捕获了
ZodError,但email.sendMail可能抛出服务内部错误、网络错误等其他异常,未被捕获会导致请求无响应,严重影响用户体验 - 代码冗余:
ServiceError和ValidationError的构造函数中,大量默认提示语、状态码重复定义,后续新增错误类型时会持续增加冗余代码 - 错误信息不够精准:Zod本身提供了详细的字段错误描述(比如
"from must be a valid email"),但当前只返回模糊的"Invalid data",前端无法给用户展示具体的错误指引 - 扩展性限制:
ServiceError强制将key设为null,后续如果需要针对特定服务错误关联具体字段或业务标识,这个硬编码限制会阻碍功能扩展
3. 具体改进方向
(1)保留原始错误栈
捕获Zod错误时直接传递原始错误的栈信息:
const validationError = new ValidationError({ key: String(error.issues[0].path[0]), message: error.issues[0].message, // 使用Zod的具体错误提示 stack: error.stack, statusCode: 400, action: "请检查对应字段的输入格式", });
(2)补充全局错误捕获
在catch块中增加默认分支,处理所有未被捕获的错误:
catch (error) { if (error instanceof ZodError) { // 现有Zod错误处理逻辑 } else { const serviceError = new ServiceError({ message: error instanceof Error ? error.message : "服务内部错误", stack: error instanceof Error ? error.stack : new Error().stack, statusCode: 500, action: "请稍后重试,若问题持续请联系管理员", }); return new Response( JSON.stringify({ error: serviceError }), { status: serviceError.statusCode } ); } }
(3)优化BaseError减少冗余
将重复的默认值逻辑移到BaseError中,子类仅传递差异化参数:
class BaseError extends Error { public statusCode: number; public action?: string; public key?: string; constructor({ name = "BaseError", message = "发生了未知错误", action = "请稍后重试", statusCode = 500, stack, key, }: Partial<BaseErrorProperties>) { super(message); // 正确调用Error构造函数,初始化message和name this.name = name; this.statusCode = statusCode; this.action = action; this.stack = stack || new Error().stack; this.key = key; } } class ServiceError extends BaseError { constructor(props: Partial<BaseErrorProperties>) { super({ name: "ServiceError", message: "服务暂时不可用,请稍后重试", statusCode: 503, ...props, key: null, }); } }
(4)保留Zod错误的完整信息
在ValidationError中扩展issues字段,传递所有校验失败的细节,支持前端展示多字段错误:
import { ZodIssue } from "zod"; class ValidationError extends BaseError { public issues?: ZodIssue[]; constructor({ message, action, statusCode, stack, key, issues, }: BaseErrorProperties & { issues?: ZodIssue[] }) { super({ name: "ValidationError", message: message || "数据校验失败", statusCode: statusCode || 400, action: action || "请检查输入数据", stack, key, }); this.issues = issues; } } // 捕获Zod错误时传入完整issues const validationError = new ValidationError({ key: String(error.issues[0].path[0]), message: error.issues[0].message, stack: error.stack, issues: error.issues, });
内容的提问来源于stack exchange,提问作者tintin
相关产品推荐
相关产品推荐

