用 ts-results 改善JS/TS错误处理,告别“吞错误”

在日常的前端开发中,我们往往更关注"业务逻辑正确路径"的实现,而忽略了对业务错误的处理。久而久之,代码中出现了以下问题:

  • 错误处理不统一:不同人处理错误的方式不一致,有的 throw,有的 return,有的直接吞掉。
  • 错误遗漏:某些错误没有被处理,导致线上异常难以追踪。
  • 错误类型混乱:错误信息不规范,缺乏结构化,无法统一处理。

为了解决这些问题,我尝试引入了一个概念:Checked Exception(受检异常) ,并通过 TypeScript 库 ts-results 来模拟这一模式,实现更清晰、更可控的错误处理方式。

一、什么是 Checked Exception?和 try/catch 有什么区别?

1. Unchecked Exception(非受检异常)------我们常用的 try/catch

  • 在 JS 中,我们常用 try/catch 来捕获异常,简单直接。
  • 它属于 Unchecked Exception ,意思是:你不需要显式声明可能会抛出的错误类型
  • 这虽然方便,但也容易导致错误处理被遗漏,尤其是多人协作的项目。

2. Checked Exception(受检异常)------强制处理错误

  • 这个概念来源于 Java、Rust 等语言。
  • 它的核心思想是:函数返回值中明确包含"成功"或"失败"的类型,调用者必须处理所有可能的错误情况。
  • 在 JS/TS 中,我们无法直接使用 Checked Exception,但可以用 ts-results 来模拟。

二、ts-results 是什么?怎么用?

ts-results 是一个轻量级库,模拟了 Rust 中的 Result<T, E> 类型。它的核心是:

ts 复制代码
type Result<T, E> = Ok<T> | Err<E>;

你可以把它理解为一个"带错误信息的成功/失败结果",调用者必须判断是成功还是失败。

1. 安装

复制代码
npm install ts-results

2. 基本用法

ts 复制代码
import { Ok, Err, Result } from 'ts-results';

function divide(a: number, b: number): Result<number, string> {
  if (b === 0) {
    return new Err('除数不能为0');
  }
  return new Ok(a / b);
}

const result = divide(10, 0);

if (result.ok) {
  console.log('结果是:', result.val);
} else {
  console.error('出错了:', result.val);
}

三、ts-results 的好处

强制处理错误

  • 使用 Result 返回值后,开发者必须显式处理成功和失败两种情况。
  • 不像 throw 那样可以"漏掉",也不像 try/catch 那样容易"吞掉"。

类型安全

  • Result<T, E> 中的 TE 都是类型,TypeScript 会帮你做类型推导。
  • 例如,如果你返回的是 Result<User, AuthError>,那调用者就知道可能的错误类型是 AuthError

链式处理错误

  • map, mapErr, andThen 等方法让错误处理更简洁。
  • 举个例子:
ts 复制代码
function getUser(): Result<User, Error> { ... }

function getUserName(): Result<string, Error> {
  return getUser()
    .map(user => user.name);
}

四、如何在项目中落地 ts-results?

我们可以把一个页面的技术架构分为几个层次,每一层都使用 Result 来处理错误:

在我们的项目中,一个页面一般从下到上分为 5 层:

  1. 服务层 - HTTP 服务
  2. 服务层 - 接口对接
  3. 服务层 - 数据处理
  4. 交互层 - 数据 Store
  5. 交互层 - UI 组件

在服务层的 HTTP 服务与接口对接会根据系统与业务的要求设定预先设定好的 Error,例如:Axios Error,请求参数错误 Error,远程处理错误 Error,预定义的带有错误码的 Error 等等。

在数据处理层,会对下层的 Error 进行聚合与预处理,例如将 Axios Error,请求参数错误 Error,远程处理错误 Error 聚合,产生一个新的错误,系统服务 Error。对其他的错误放行

在数据 Store 中会处理下层传上来的所有 Error,按照业务需要分别处理,对无法处理的错误 unwrap,throw 到顶层,由 sentry 捕获

服务层 - HTTP 请求封装

ts 复制代码
function processResponse(url: string, resp: AxiosResponse) {
  if (resp && resp.status === 200) {
    if (resp.data.IsSuccess) {
      return new Ok(resp.data.Value);
    } else {
      return new Err(new CustomRespError(resp.data.Code, resp.data.Message, url));
    }
  } else {
    if (resp.status === 404) {
      return new Err(new PageRedirectError(404, ''));
    } else if ((resp.status = 403)) {
      return new Err(new PageRedirectError(403, ''));
    } else if (resp.status === 401) {
      return new Err(new NoLoginError(resp.statusText));
    } else {
      return new Err(
        new PageRedirectError(500, `服务错误, 请稍后重试, 状态码 [${resp.status}]`),
      );
    }
  }
}

function processError(e: Error) {
  if (axios.isAxiosError(e)) {
    e as AxiosError;
    if (e.code === AxiosError.ERR_NETWORK || e.code === AxiosError.ETIMEDOUT) {
      return new Err(new NetworkError(e.message));
    }
    ...
}

export async function get<T>(url: string): Promise<Result<T, Error>> {
  try {
    return processResponse(url, await instance.get<Resp<T>>(url));
  } catch (e: any) {
    return processError(e);
  }
}

export async function post<T>(url: string, data: any): Promise<Result<T, Error>> {
  try {
    return processResponse(url, await instance.post<Resp<T>>(url, data));
  } catch (e: any) {
    return processError(e);
  }
}

可以看到在这里,对系统和接口返回的错误进行了封装,分别分装为:

* PageRedirectError 需要页面跳转类的错误

* NoLoginError 未登录类的错误

* NetworkError 系统网络类错误

* CustomRespError 业务预定义错误

错误的定义

js 复制代码
export class PageRedirectError extends Error {
  status: number;
  msg: string;

  constructor(status: number, msg: string) {
    super(`PageRedirect: ${status} - ${msg}`);
    this.status = status;
    this.msg = msg;
  }

  toString() {
    return `网络错误(${this.status}),请刷新后重试`;
  }
}

...

export class CustomRespError extends Error {
  code: string;
  msg: string;
  path: string;

  constructor(code: string, msg: string, path: string) {
    super(`CustomRespError: ${code}`);
    this.code = code;
    this.msg = msg;
    this.path = path;
  }

  toString() {
    return this.msg;
  }

  static checkCode(err: Error, code: string): boolean {
    if (err instanceof CustomRespError) {
      return err.code === code;
    }

    return false;
  }
}

接口对接层 - 业务逻辑封装

ts 复制代码
async function fetchList(): Promise<Result<List, Error>> {
  const result = await get('/api/list');
  return result.andThen(data => {
    if (data.list.length === 0) {
      return new Err(new CustomRespError('DATA-0000', '无数据', '');
    }
    return new Ok(data);
  });
}

交互层

js 复制代码
export class State {
  ...

  constructor() {
    makeAutoObservable(this);
  }

  mounted() {
    ...
    this.getTodoTasks();
  }

  async getTodoTasks() {
    this.pageLoading = true;
    this.todoState.getTodoCount()
    const result = await getTodoTasks({
      ...this.todoQuery,
      SortType: this.sortType
    });
    if (result.ok) {
      this.list = result.val.datas;
      this.total = result.val.total;
    } else {
      if (CustomRespError.checkCode(result.val, 'DATA-000')) {
        // 业务处理逻辑,例如弹窗等等
      } else {
        // 这里使用通用的方法对错误进行统一处理
        this.errorState.catchError(result);
      }
      // 如果您实在是不知道如何处理,请使用 result.unwrap() 将 Checked Exception 转换为 RunTime Exception
    }
    this.pageLoading = false;
  }
}
export default new State();

五、使用 ts-results 的注意事项

1. 它不是万能的

  • 它只能处理你提前预定义好的错误类型
  • 对于运行时错误(如数组越界、undefined 属性访问等)仍然需要 try/catch 来兜底。

2. 错误类型尽量继承 Error

  • 推荐使用 new Err(new MyError(...)) 而不是 new Err('string')
  • 原因是 Error 类型可以被 Sentry 等工具识别,方便日志和上报。

3. 所有 Result 都应该被处理

  • 不要"吞掉"错误,即不要写:

    ts 复制代码
    const result = doSomething();
    // 错误没有处理
  • 应该处理掉,或者 unwrap() 抛出错误:

    ts 复制代码
    const result = doSomething();
    if (!result.ok) {
      result.unwrap(); // 抛出错误,由上层 catch
    }

六、总结:ts-results 的价值

优势 说明
强制错误处理 调用者必须处理所有可能的错误路径
类型安全 TypeScript 支持良好的类型推导
结构清晰 错误不再是"字符串",而是结构化对象
易于调试 错误信息可追踪、可上报、可分类

最后

ts-results 并不是银弹,但它能帮助我们构建一个更清晰、更健壮的错误处理体系。特别是在大型项目中,它能显著提升代码的可维护性和错误的可追踪性。

如果你的项目中经常出现"错误没处理"、"错误类型混乱"、"错误被吞掉"的问题,不妨试试 ts-results,它可能是你迈向高质量前端代码的一小步,却是非常关键的一步。

相关推荐
刘大猫.几秒前
npm ERR! cb() never called!
前端·npm·node.js·npm install·npmm err·never called
咔咔一顿操作4 分钟前
常见问题三
前端·javascript·vue.js·前端框架
前端程序媛Ying5 分钟前
点击按钮滚动到底功能vue的v-on:scroll运用
javascript
上单带刀不带妹5 分钟前
Web Worker:解锁浏览器多线程,提升前端性能与体验
前端·js·web worke
电商API大数据接口开发Cris21 分钟前
Node.js + TypeScript 开发健壮的淘宝商品 API SDK
前端·数据挖掘·api
还要啥名字24 分钟前
基于elpis下 DSL有感
前端
一只毛驴29 分钟前
谈谈浏览器的DOM事件-从0级到2级
前端·面试
用户81686947472531 分钟前
封装ajax
前端
pengzhuofan32 分钟前
Web开发系列-第13章 Vue3 + ElementPlus
前端·elementui·vue·web
yvvvy32 分钟前
白嫖 React 性能优化?是的,用 React.memo!
前端·javascript