TypeScript 规范项目错误处理
在 JavaScript 开发中,通常都不太重视起错误处理,捕获和记录错误对于任何项目的开发周期都是至关重要的。随着 TypeScript 项目开发多了,开始意识到并不真正了解错误处理。经常在项目代码中看到一下类似代码:
try {
throw new Error("Oops")
} catch (error) {
console.error(error.message)
}
error
是 unknown
类型 ,因此在将其转换为新类型或缩小类型范围之前,不能对 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 的工具记录跟踪错误。