TypeScript 规范项目错误处理

TypeScript 规范项目错误处理

在 JavaScript 开发中,通常都不太重视起错误处理,捕获和记录错误对于任何项目的开发周期都是至关重要的。随着 TypeScript 项目开发多了,开始意识到并不真正了解错误处理。经常在项目代码中看到一下类似代码:

try {
    throw new Error("Oops")
} catch (error) {
    console.error(error.message)
}

errorunknown 类型 ,因此在将其转换为新类型或缩小类型范围之前,不能对 error 执行任何操作。正确的处理方式是缩小类型,将看看如何做到这一点,但为什么这是必要的?

在 JavaScript 中,几乎任何东西都可以被抛出:

throw "oops"
throw 210
throw null
throw { message: "异常错误" }

所以真正被捕获的错误是未知的。但是,可以通过使用 TypeScript 的多种方式干净地处理错误。

JavaScript错误的基础知识

JavaScript 中的错误类型,在 JavaScript 中有许多类型的错误,但最常见的是:

  • ReferenceError:代码引用了一个不存在的变量。
  • TypeError:值不是预期的错误类型
  • SyntaxError:代码在语法上无效

抛出错误

有时需要手动抛出错误,例如,可能有一些代码依赖于函数调用的返回值,但有可能该值是 undefined,或者至少在 TypeScript 认为是 undefined。在下面这个例子中,抛出是缩小返回用户范围的最佳解决方案。

// 通常方式
function createProject() {
    const user = getUser();
    saveProject({ name: "", userId: user.id })
}
// 避免异常
function createProject() {
    const user = getUser();
    if (!user) {
        return;
    }
    saveProject({ name: "", userId: user.id })
}
// 最佳方式,抛出异常
function createProject() {
    const user = getUser();
    if (!user) {
        throw new ReferenceError('用户不存在')
    }
    saveProject({ name: "", userId: user.id })
}

捕获错误

一旦错误被抛出,它将在调用堆栈中冒泡,直到被 try/catch 语句捕获。当在 try 块内运行的代码抛出错误时,它将在 catch 块中被捕获,错误可能源自嵌套在函数内部的函数,并且会冒泡直到被捕获。

try {
    throw new ReferenceError();
} catch (error) {
    console.error(error)
}

缩小错误类型

一旦被捕获,检查所抛出的错误类型可能很有用。这使能够将类型从未知缩小到可以与之交互的特定类型(可以直观的理解错误),可以用 instanceof 做到这一点:

try {
    throw new ReferenceError();
} catch (error) {
    if (error instanceof ReferenceError) {
        console.error(error.message)
    }
}

设计模式

设计模式是软件设计中常见问题的解决方案,这些模式很容易重复使用并且富有表现力。在最新的项目中,将代码按域分组在名为 Features 的目录中,它可以包含相关的组件、钩子、类型、错误等等,每个 Feature 目录都包含一个 errors.ts 文件,在其中为各自的域定义了一个自定义错误类。

创建自定义错误类型

errors.ts 文件中,导出了一个 class。为潜在名称维护一个联合类型,这增加了一些不错的智能感知和类型安全。该类扩展了 Error 对象,它允许插入堆栈跟踪(对于大多数 JS 运行时)。

type ErrorName =
    'GET_PROJECT_ERROR' | 'CREATE_PROJECT_ERROR' | 'PROJECT_LIMIT_REACHED';

export class ProjectError extends Error {
    name: ErrorName;
    message: string;
    cause: any;

    constructor({ name, message, cause }: {
        name: ErrorName;
        message: string;
        cause?: any;
    }) {
        super();
        this.name = name;
        this.message = message;
        this.cause = cause;
    }
}

抛出自定义错误

实例化新错误时,name 值具有智能感知,并且必须是联合类型中定义的名称之一。

export async function createProject() {
    const { data, error } = await api.createProject();

    if (error) {
        throw new ProjectError({
            name: "CREATE_PROJECT_ERROR",
            message: "API error occurred while creating project",
            cause: error
        })
    }

    if (data.length === projectLimit) {
        throw new ProjectError({
            name: "PROJECT_LIMIT_REACHED",
            message: "Project limit has been reached."
        })
    }

    return data;
}

捕获自定义错误

当错误被捕获时,可以使用 instanceof 缩小错误类型。一旦缩小范围,error.name 就会智能感知,此时可以根据抛出的错误名称执行逻辑。在此示例中,PROJECT_LIMIT_REACHED 错误是要向用户显示的错误,提供了一条专门为用户呈现的消息。

try {
    await createProject();
} catch (error) {
    if (error instanceof ProjectError) {
        if (error.name === "PROJECT_LIMIT_REACHED") {
            toast(error.message)
        }
    }
}

定义可重用的错误库

由于项目中有很多 errors.ts 文件,类中唯一的动态代码是名称的联合类型,因此可以对代码进行优化,创建了一个 ErrorBase 类,它接受用作名称类型的泛型。

export class ErrorBase<T extends string> extends Error {
    name: T;
    message: string;
    cause: any;

    constructor({ name, message, cause }: { name: T, message: string, cause?: any }) {
        super();
        this.name = name;
        this.message = message;
        this.cause = cause
    }
}

现在,当创建一个新的自定义错误类时,可以扩展这个基类,需要做的就是给它提供可用名称的联合类型。

import { ErrorBase } from "./error-base"
type ErrorName =
    'GET_PROJECT_ERROR' | 'CREATE_PROJECT_ERROR' | 'PROJECT_LIMIT_REACHED';

export class TeamError extends ErrorBase<ErrorName>{ }

总结

设计模式让代码变得更容易维护,处理错误只是维护良好的应用程序的一部分,另一个重要步骤是使用类似 Sentry 的工具记录跟踪错误。