You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 10:59:53